import { z } from 'zod' import { TRPCError } from '@trpc/server' import { router, protectedProcedure } from '../trpc' import { getStorageProviderWithType, createStorageProvider, generateAvatarKey, getContentType, isValidImageType, type StorageProviderType, } from '@/lib/storage' export const avatarRouter = router({ /** * Get a pre-signed URL for uploading an avatar */ getUploadUrl: protectedProcedure .input( z.object({ fileName: z.string(), contentType: z.string(), }) ) .mutation(async ({ ctx, input }) => { // Validate content type if (!isValidImageType(input.contentType)) { throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid image type. Allowed: JPEG, PNG, GIF, WebP' }) } const userId = ctx.user.id const key = generateAvatarKey(userId, input.fileName) const contentType = getContentType(input.fileName) const { provider, providerType } = await getStorageProviderWithType() const uploadUrl = await provider.getUploadUrl(key, contentType) return { uploadUrl, key, providerType, // Return so client can pass it back on confirm } }), /** * Confirm avatar upload and update user profile */ confirmUpload: protectedProcedure .input( z.object({ key: z.string(), providerType: z.enum(['s3', 'local']), }) ) .mutation(async ({ ctx, input }) => { const userId = ctx.user.id // Use the provider that was used for upload const provider = createStorageProvider(input.providerType) const exists = await provider.objectExists(input.key) if (!exists) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Upload not found. Please try uploading again.' }) } // Delete old avatar if exists (from its original provider) const currentUser = await ctx.prisma.user.findUnique({ where: { id: userId }, select: { profileImageKey: true, profileImageProvider: true }, }) if (currentUser?.profileImageKey) { try { const oldProvider = createStorageProvider( (currentUser.profileImageProvider as StorageProviderType) || 's3' ) await oldProvider.deleteObject(currentUser.profileImageKey) } catch (error) { // Log but don't fail if old avatar deletion fails console.warn('Failed to delete old avatar:', error) } } // Update user with new avatar key and provider const user = await ctx.prisma.user.update({ where: { id: userId }, data: { profileImageKey: input.key, profileImageProvider: input.providerType, }, select: { id: true, profileImageKey: true, profileImageProvider: true, }, }) // Audit log await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'UPDATE', entityType: 'User', entityId: userId, detailsJson: { field: 'profileImageKey', newValue: input.key, provider: input.providerType, }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return user }), /** * Get the current user's avatar URL */ getUrl: protectedProcedure.query(async ({ ctx }) => { const userId = ctx.user.id const user = await ctx.prisma.user.findUnique({ where: { id: userId }, select: { profileImageKey: true, profileImageProvider: true }, }) if (!user?.profileImageKey) { return null } // Use the provider that was used when the file was stored const providerType = (user.profileImageProvider as StorageProviderType) || 's3' const provider = createStorageProvider(providerType) const url = await provider.getDownloadUrl(user.profileImageKey) return url }), /** * Delete the current user's avatar */ delete: protectedProcedure.mutation(async ({ ctx }) => { const userId = ctx.user.id const user = await ctx.prisma.user.findUnique({ where: { id: userId }, select: { profileImageKey: true, profileImageProvider: true }, }) if (!user?.profileImageKey) { return { success: true } } // Delete from the provider that was used when the file was stored const providerType = (user.profileImageProvider as StorageProviderType) || 's3' const provider = createStorageProvider(providerType) try { await provider.deleteObject(user.profileImageKey) } catch (error) { console.warn('Failed to delete avatar from storage:', error) } // Update user - clear both key and provider await ctx.prisma.user.update({ where: { id: userId }, data: { profileImageKey: null, profileImageProvider: null, }, }) // Audit log await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'DELETE', entityType: 'User', entityId: userId, detailsJson: { field: 'profileImageKey' }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return { success: true } }), })