From b41bd8a3eb4c3f1ebe048d580132f65448a387ae Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Thu, 29 Jan 2026 00:02:59 +0530 Subject: [PATCH] feat: Implement authentication checks for profile actions and user layout, and add presigned URL fetching for file viewing. --- .../agent/edit/components/DynamicField.tsx | 67 +++++++++++++++---- src/app/(auth)/signup/page.tsx | 62 ++++++++++++++++- src/app/(user)/layout.tsx | 42 +++++++++++- src/app/page.tsx | 19 +++--- src/components/profile/ProfileCard.tsx | 26 +++++++ src/components/profile/StatusButtons.tsx | 28 +++++++- src/middleware.ts | 6 +- src/services/api.ts | 32 +++++++-- src/services/auth.service.ts | 1 + 9 files changed, 243 insertions(+), 40 deletions(-) diff --git a/src/app/(agent)/agent/edit/components/DynamicField.tsx b/src/app/(agent)/agent/edit/components/DynamicField.tsx index fd3c71f..fcc894b 100644 --- a/src/app/(agent)/agent/edit/components/DynamicField.tsx +++ b/src/app/(agent)/agent/edit/components/DynamicField.tsx @@ -444,6 +444,7 @@ function RangeSlider({ field, value, onChange }: RangeSliderProps) { // File Upload Types interface UploadedFile { id: string; + key?: string; // S3 key for fetching presigned URL name: string; size: number; type: string; @@ -459,6 +460,12 @@ interface UploadingFile { error?: string; } +// Map to store presigned URLs by file ID +interface FileWithPresignedUrl extends UploadedFile { + presignedUrl?: string; + isLoadingUrl?: boolean; +} + // File Upload Component interface FileUploadProps { field: ProfileField; @@ -471,6 +478,8 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) { const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [uploadingFiles, setUploadingFiles] = useState([]); + const [presignedUrls, setPresignedUrls] = useState>({}); + const [loadingUrls, setLoadingUrls] = useState>({}); // Get allowed formats from field config or use defaults const allowedFormats = field.validation?.allowedFormats || ['PDF', 'DOCX', 'JPG', 'PNG']; @@ -631,6 +640,42 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) { fileInputRef.current?.click(); }; + const handleViewFile = async (file: UploadedFile) => { + // Check if we already have a presigned URL + if (presignedUrls[file.id]) { + window.open(presignedUrls[file.id], '_blank'); + return; + } + + // Get the key - either from file.key, file.id, or file.url if it's an S3 key + const fileKey = file.key || file.id || file.url; + + // If the URL is already a full URL (http/https), just open it directly + if (fileKey.startsWith('http://') || fileKey.startsWith('https://')) { + window.open(fileKey, '_blank'); + return; + } + + // Set loading state + setLoadingUrls(prev => ({ ...prev, [file.id]: true })); + + try { + const { uploadService } = await import('@/services/upload.service'); + const presignedUrl = await uploadService.getPresignedDownloadUrl(fileKey); + + // Store for future use + setPresignedUrls(prev => ({ ...prev, [file.id]: presignedUrl })); + + // Open the file + window.open(presignedUrl, '_blank'); + } catch (error) { + console.error('Failed to get presigned URL:', error); + alert('Failed to open file. Please try again.'); + } finally { + setLoadingUrls(prev => ({ ...prev, [file.id]: false })); + } + }; + const isUploading = uploadingFiles.length > 0; return ( @@ -744,20 +789,14 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) { >
- {file.url ? ( - - {file.name} - - ) : ( - - {file.name} - - )} +
+ {/* Agent Type Selection - Only for Agents */} + {userType === 'AGENT' && ( +
+ + {getFieldError('agentTypeId') && ( +

{getFieldError('agentTypeId')}

+ )} +
+ )} + {/* Sign Up Button */}
@@ -21,7 +42,10 @@ export function StatusButtons({ isAvailable = true }: StatusButtonsProps) { Unavailable. - diff --git a/src/middleware.ts b/src/middleware.ts index 84cf931..256cca4 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -5,7 +5,7 @@ import { NextResponse } from "next/server"; const authRoutes = ["/login", "/signup", "/forgot-password", "/reset-password", "/verify-email"]; // Public routes - accessible to everyone (logged in or not) -const publicRoutes = ["/", "/contact", "/about", "/faq"]; +const publicRoutes = ["/", "/contact", "/about", "/faq", "/user/dashboard", "/user/profiles", "/user/profile"]; export default auth((req) => { const { nextUrl } = req; @@ -58,8 +58,8 @@ export default auth((req) => { return NextResponse.redirect(new URL("/user/dashboard", nextUrl.origin)); } - // Prevent agents from accessing user routes - if (isUserRoute && userRole === "AGENT") { + // Prevent agents from accessing protected user routes (but allow public user routes) + if (isUserRoute && userRole === "AGENT" && !isPublicRoute) { return NextResponse.redirect(new URL("/agent/dashboard", nextUrl.origin)); } } diff --git a/src/services/api.ts b/src/services/api.ts index 5e2621c..13ad7f8 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -62,6 +62,19 @@ api.interceptors.request.use( } ); +// Public API endpoints that don't require authentication +const publicEndpoints = [ + '/agents', + '/agent-types', + '/profiles', +]; + +// Check if URL is a public endpoint +const isPublicEndpoint = (url?: string): boolean => { + if (!url) return false; + return publicEndpoints.some(endpoint => url.includes(endpoint)); +}; + // Response interceptor with token refresh api.interceptors.response.use( (response) => { @@ -72,6 +85,17 @@ api.interceptors.response.use( // Handle 401 Unauthorized if (error.response?.status === 401 && !originalRequest._retry) { + // For public endpoints, just reject the error without redirecting to signout + // The request will work without auth anyway + if (isPublicEndpoint(originalRequest.url)) { + // Remove the auth header and retry once + if (originalRequest.headers) { + delete originalRequest.headers.Authorization; + } + originalRequest._retry = true; + return api(originalRequest); + } + // Don't try to refresh if this was the refresh token request itself if (originalRequest.url?.includes('/auth/refresh')) { // Refresh token is also invalid, clear everything and sign out properly @@ -105,13 +129,11 @@ api.interceptors.response.use( const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; if (!refreshToken) { - // No refresh token available, sign out properly + // No refresh token available, just clear tokens but don't redirect for better UX if (typeof window !== 'undefined') { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); - // Use NextAuth's signout endpoint to properly clear the session - window.location.href = '/api/auth/signout?callbackUrl=/login'; } isRefreshing = false; return Promise.reject(error); @@ -145,13 +167,11 @@ api.interceptors.response.use( // Retry original request return api(originalRequest); } catch (refreshError) { - // Refresh failed, clear tokens and sign out properly + // Refresh failed, clear tokens but don't redirect if (typeof window !== 'undefined') { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); - // Use NextAuth's signout endpoint to properly clear the session - window.location.href = '/api/auth/signout?callbackUrl=/login'; } processQueue(refreshError as AxiosError); isRefreshing = false; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 36b734f..e5e93be 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -8,6 +8,7 @@ export interface RegisterRequest { email: string; password: string; role: 'USER' | 'AGENT'; + agentTypeId?: string; } export interface LoginRequest {