import { toAdminInventoryImageDto } from '@pokemon-inventory/contracts/image'; import { createInventoryImageSchema, moveInventoryImageSchema, updateImageOrderSchema, updateImageAltTextSchema } from '@pokemon-inventory/validation/image'; import { randomUUID } from 'node:crypto'; import { createApplicationError } from '../../../shared/errors/application-error.js'; function validationError(reply, result) { return reply.code(400).send({ error: { code: 'VALIDATION_ERROR', message: 'Request validation failed.', details: result.error.issues.map(({ message, path }) => ({ message, path })), }, }); } export async function registerAdminImageRoutes(app, options) { const { authenticateAdmin, authorizeUnsafeRequest, sessionCookieName = 'pokemon_inventory_session', inventoryImageService, } = options; app.post('/inventory/:id/images', async (request, reply) => { const admin = await authenticateAdmin(request); if (!admin?.sellerId) { throw createApplicationError('UNAUTHORIZED', 'Authentication is required.', 401); } await authorizeUnsafeRequest?.( request.cookies[sessionCookieName], request.headers['x-csrf-token'], ); if (request.isMultipart()) { const part = await request.file(); if (!part) { throw createApplicationError('IMAGE_FILE_REQUIRED', 'An image file is required.', 400); } const data = await part.toBuffer(); const fields = part.fields; const originalFilename = part.filename; const result = createInventoryImageSchema.safeParse({ storageKey: `inventory/${request.params.id}/${randomUUID()}`, originalFilename, mimeType: part.mimetype, fileSize: data.length, width: Number(fields.width?.value), height: Number(fields.height?.value), altText: fields.altText?.value, sortOrder: fields.sortOrder ? Number(fields.sortOrder.value) : undefined, isPrimary: fields.isPrimary?.value === 'true', }); if (!result.success) return validationError(reply, result); const image = await inventoryImageService.upload({ ...result.data, data, inventoryItemId: request.params.id, sellerId: admin.sellerId, }); return reply.code(201).send({ data: toAdminInventoryImageDto(image) }); } const result = createInventoryImageSchema.safeParse(request.body); if (!result.success) return validationError(reply, result); const image = await inventoryImageService.create({ ...result.data, inventoryItemId: request.params.id, sellerId: admin.sellerId, }); return reply.code(201).send({ data: toAdminInventoryImageDto(image) }); }); app.get('/inventory/:id/images', async (request) => ({ data: (await inventoryImageService.list({ inventoryItemId: request.params.id, sellerId: (await authenticateAdmin(request)).sellerId, })).map(toAdminInventoryImageDto), })); async function authorizeAdminMutation(request) { const admin = await authenticateAdmin(request); if (!admin?.sellerId) { throw createApplicationError('UNAUTHORIZED', 'Authentication is required.', 401); } await authorizeUnsafeRequest?.( request.cookies[sessionCookieName], request.headers['x-csrf-token'], ); return admin; } app.post('/images/:id/move', async (request, reply) => { const admin = await authorizeAdminMutation(request); const result = moveInventoryImageSchema.safeParse(request.body); if (!result.success) return validationError(reply, result); const image = await inventoryImageService.move({ imageId: request.params.id, direction: result.data.direction, sellerId: admin.sellerId, }); return { data: toAdminInventoryImageDto(image) }; }); app.patch('/images/:id/order', async (request, reply) => { const admin = await authorizeAdminMutation(request); const result = updateImageOrderSchema.safeParse(request.body); if (!result.success) return validationError(reply, result); const image = await inventoryImageService.updateOrder({ imageId: request.params.id, sortOrder: result.data.sortOrder, sellerId: admin.sellerId, }); return { data: toAdminInventoryImageDto(image) }; }); app.post('/images/:id/primary', async (request) => { const admin = await authorizeAdminMutation(request); const image = await inventoryImageService.setPrimary({ imageId: request.params.id, sellerId: admin.sellerId, }); return { data: toAdminInventoryImageDto(image) }; }); app.patch('/images/:id/alt-text', async (request, reply) => { const admin = await authorizeAdminMutation(request); const result = updateImageAltTextSchema.safeParse(request.body); if (!result.success) return validationError(reply, result); const image = await inventoryImageService.updateAltText({ imageId: request.params.id, altText: result.data.altText, sellerId: admin.sellerId, }); return { data: toAdminInventoryImageDto(image) }; }); app.post('/images/:id/retry', async (request) => { const admin = await authorizeAdminMutation(request); const image = await inventoryImageService.retry({ imageId: request.params.id, sellerId: admin.sellerId, }); return { data: toAdminInventoryImageDto(image) }; }); app.delete('/images/:id', async (request, reply) => { const admin = await authorizeAdminMutation(request); await inventoryImageService.delete({ imageId: request.params.id, sellerId: admin.sellerId }); return reply.code(204).send(); }); }