From ba29257ba521b6dd58599eb01422562eb6f8ef99 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Mon, 26 Jan 2026 22:43:41 +0530 Subject: [PATCH] feat: Implement S3 presigned URL fetching for user avatars and add an image proxy utility. --- src/app/dashboard/users/page.tsx | 44 +++++++++++++++++++++++++++++--- src/lib/imageProxy.ts | 35 +++++++++++++++++++++++++ src/services/index.ts | 3 +++ src/services/upload.service.ts | 27 ++++++++++++++++++++ 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/lib/imageProxy.ts create mode 100644 src/services/upload.service.ts diff --git a/src/app/dashboard/users/page.tsx b/src/app/dashboard/users/page.tsx index b0cb28c..e4abbb0 100644 --- a/src/app/dashboard/users/page.tsx +++ b/src/app/dashboard/users/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { usersService, User, getErrorMessage } from '@/services'; +import { usersService, User, getErrorMessage, uploadService } from '@/services'; type RoleFilter = 'ALL' | 'ADMIN' | 'USER' | 'AGENT'; @@ -21,8 +21,40 @@ export default function UsersPage() { const [currentPage, setCurrentPage] = useState(1); const [totalUsers, setTotalUsers] = useState(0); const [activeFilter, setActiveFilter] = useState('ALL'); + const [avatarUrls, setAvatarUrls] = useState>({}); const limit = 10; + // Helper function to check if avatar is an S3 key + const isS3Key = (avatar: string | null | undefined): boolean => { + if (!avatar) return false; + return !avatar.startsWith('http') && !avatar.startsWith('/'); + }; + + // Fetch presigned URLs for user avatars + const fetchAvatarUrls = async (userList: User[]) => { + const urls: Record = {}; + + await Promise.all( + userList.map(async (user) => { + const avatar = user.profile?.avatar; + if (avatar) { + if (isS3Key(avatar)) { + try { + const presignedUrl = await uploadService.getPresignedDownloadUrl(avatar); + urls[user.id] = presignedUrl; + } catch (err) { + console.error(`Failed to get avatar URL for user ${user.id}:`, err); + } + } else { + urls[user.id] = avatar; + } + } + }) + ); + + setAvatarUrls(urls); + }; + useEffect(() => { fetchUsers(); }, [currentPage, activeFilter]); @@ -36,8 +68,12 @@ export default function UsersPage() { filters.role = activeFilter; } const response = await usersService.getUsers(filters); - setUsers(response.users || []); + const userList = response.users || []; + setUsers(userList); setTotalUsers(response.total || 0); + + // Fetch presigned URLs for avatars + await fetchAvatarUrls(userList); } catch (err) { const errorMessage = getErrorMessage(err); setError(errorMessage); @@ -167,10 +203,10 @@ export default function UsersPage() {
- {user.profile?.avatar ? ( + {avatarUrls[user.id] ? ( ) : ( diff --git a/src/lib/imageProxy.ts b/src/lib/imageProxy.ts new file mode 100644 index 0000000..fcb3829 --- /dev/null +++ b/src/lib/imageProxy.ts @@ -0,0 +1,35 @@ +/** + * Convert avatar URL to proper backend URL + * Handles both S3 URLs and relative backend paths + */ +export function getProxyImageUrl(url: string | null | undefined): string { + if (!url) { + return ''; + } + + // If it's a data URL, return as-is + if (url.startsWith('data:')) { + return url; + } + + const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1'; + const apiBaseUrl = apiUrl.replace('/api/v1', ''); + + // If it's a relative path from backend (like /development/avatars/...) + // Prefix with backend base URL + if (url.startsWith('/development/') || url.startsWith('/uploads/') || url.startsWith('/avatars/')) { + return `${apiBaseUrl}${url}`; + } + + // If it's an S3/external URL, proxy through backend + if (url.startsWith('http')) { + return `${apiUrl}/upload/image?url=${encodeURIComponent(url)}`; + } + + // For other relative paths, assume they're backend paths + if (url.startsWith('/')) { + return `${apiBaseUrl}${url}`; + } + + return url; +} diff --git a/src/services/index.ts b/src/services/index.ts index a687f35..2f34c0c 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -53,3 +53,6 @@ export type { CreateFieldDto, UpdateFieldDto, } from './profile-fields.service'; + +// Upload Service +export { uploadService } from './upload.service'; diff --git a/src/services/upload.service.ts b/src/services/upload.service.ts new file mode 100644 index 0000000..a73dd4e --- /dev/null +++ b/src/services/upload.service.ts @@ -0,0 +1,27 @@ +import api from './api'; + +class UploadService { + private basePath = '/upload'; + + /** + * Get a presigned download URL for an S3 key + */ + async getPresignedDownloadUrl(key: string): Promise { + const response = await api.get<{ data: { url: string } }>( + `${this.basePath}/presigned-download-url`, + { params: { key } } + ); + return response.data.data.url; + } + + /** + * Check if a URL is an S3 key (not a full URL or local path) + */ + isS3Key(url: string | null | undefined): boolean { + if (!url) return false; + // S3 keys don't start with http or / + return !url.startsWith('http') && !url.startsWith('/'); + } +} + +export const uploadService = new UploadService();