import { z } from 'zod' import { TRPCError } from '@trpc/server' import { router, adminProcedure } from '../trpc' import { getStorageProviderWithType, createStorageProvider, generateLogoKey, getContentType, isValidImageType, type StorageProviderType, } from '@/lib/storage' export const logoRouter = router({ /** * Get a pre-signed URL for uploading a project logo */ getUploadUrl: adminProcedure .input( z.object({ projectId: z.string(), 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' }) } // Verify project exists const project = await ctx.prisma.project.findUnique({ where: { id: input.projectId }, select: { id: true }, }) if (!project) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' }) } const key = generateLogoKey(input.projectId, 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 logo upload and update project */ confirmUpload: adminProcedure .input( z.object({ projectId: z.string(), key: z.string(), providerType: z.enum(['s3', 'local']), }) ) .mutation(async ({ ctx, input }) => { // 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 logo if exists (from its original provider) const currentProject = await ctx.prisma.project.findUnique({ where: { id: input.projectId }, select: { logoKey: true, logoProvider: true }, }) if (currentProject?.logoKey) { try { const oldProvider = createStorageProvider( (currentProject.logoProvider as StorageProviderType) || 's3' ) await oldProvider.deleteObject(currentProject.logoKey) } catch (error) { // Log but don't fail if old logo deletion fails console.warn('Failed to delete old logo:', error) } } // Update project with new logo key and provider const project = await ctx.prisma.project.update({ where: { id: input.projectId }, data: { logoKey: input.key, logoProvider: input.providerType, }, select: { id: true, logoKey: true, logoProvider: true, }, }) // Audit log await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'UPDATE', entityType: 'Project', entityId: input.projectId, detailsJson: { field: 'logoKey', newValue: input.key, provider: input.providerType, }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return project }), /** * Get a project's logo URL */ getUrl: adminProcedure .input(z.object({ projectId: z.string() })) .query(async ({ ctx, input }) => { const project = await ctx.prisma.project.findUnique({ where: { id: input.projectId }, select: { logoKey: true, logoProvider: true }, }) if (!project?.logoKey) { return null } // Use the provider that was used when the file was stored const providerType = (project.logoProvider as StorageProviderType) || 's3' const provider = createStorageProvider(providerType) const url = await provider.getDownloadUrl(project.logoKey) return url }), /** * Delete a project's logo */ delete: adminProcedure .input(z.object({ projectId: z.string() })) .mutation(async ({ ctx, input }) => { const project = await ctx.prisma.project.findUnique({ where: { id: input.projectId }, select: { logoKey: true, logoProvider: true }, }) if (!project?.logoKey) { return { success: true } } // Delete from the provider that was used when the file was stored const providerType = (project.logoProvider as StorageProviderType) || 's3' const provider = createStorageProvider(providerType) try { await provider.deleteObject(project.logoKey) } catch (error) { console.warn('Failed to delete logo from storage:', error) } // Update project - clear both key and provider await ctx.prisma.project.update({ where: { id: input.projectId }, data: { logoKey: null, logoProvider: null, }, }) // Audit log await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'DELETE', entityType: 'Project', entityId: input.projectId, detailsJson: { field: 'logoKey' }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return { success: true } }), })