feat: Implement S3 presigned URL fetching for user avatars and add an image proxy utility.

This commit is contained in:
pradeepkumar
2026-01-26 22:43:41 +05:30
parent b927d89e49
commit ba29257ba5
4 changed files with 105 additions and 4 deletions

View File

@@ -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<RoleFilter>('ALL');
const [avatarUrls, setAvatarUrls] = useState<Record<string, string>>({});
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<string, string> = {};
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() {
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
{user.profile?.avatar ? (
{avatarUrls[user.id] ? (
<img
className="h-10 w-10 rounded-full object-cover"
src={user.profile.avatar}
src={avatarUrls[user.id]}
alt=""
/>
) : (

35
src/lib/imageProxy.ts Normal file
View File

@@ -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;
}

View File

@@ -53,3 +53,6 @@ export type {
CreateFieldDto,
UpdateFieldDto,
} from './profile-fields.service';
// Upload Service
export { uploadService } from './upload.service';

View File

@@ -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<string> {
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();