diff --git a/services/platform-service/src/modules/broadcasts/repository.ts b/services/platform-service/src/modules/broadcasts/repository.ts index f8257847..63267891 100644 --- a/services/platform-service/src/modules/broadcasts/repository.ts +++ b/services/platform-service/src/modules/broadcasts/repository.ts @@ -21,14 +21,14 @@ export async function createBroadcast(doc: Broadcast): Promise { const container = getContainer('broadcasts'); const { resource } = await container.items.create(doc); if (!resource) throw new Error('Failed to create broadcast'); - return resource as Broadcast; + return resource as unknown as Broadcast; } export async function getBroadcast(id: string, productId: string): Promise { const container = getContainer('broadcasts'); try { const { resource } = await container.item(id, productId).read(); - return resource as Broadcast | null; + return resource as unknown as Broadcast | null; } catch (err) { if ((err as { code?: number }).code === 404) return null; throw err; @@ -134,7 +134,7 @@ export async function createDelivery(doc: BroadcastDelivery): Promise { const productId = getRequestProductId(req); return getFeedbackStats(productId); }); + + // ── User: Generate SAS URL for screenshot upload (Phase 1.3) ───────────── + app.post('/feedback/sas', async req => { + const userId = requireAuth(req); + const productId = getRequestProductId(req); + + // Validate content type + const body = req.body as { contentType?: 'image/png' | 'image/jpeg' | 'image/webp' }; + const validTypes = ['image/png', 'image/jpeg', 'image/webp']; + if (!body.contentType || !validTypes.includes(body.contentType)) { + throw new BadRequestError('Invalid contentType. Must be image/png, image/jpeg, or image/webp'); + } + + // Generate blob path (feedbackId will be assigned after creation) + // For pre-upload, we use a temp path that gets moved on feedback submission + const tempFeedbackId = `temp_${userId}_${Date.now()}`; + const blobPath = generateScreenshotBlobPath(productId, tempFeedbackId, body.contentType); + + // Generate SAS URL for upload (5 minutes expiry) + const uploadUrl = await generateSasUrl( + BLOB_CONTAINERS.feedbackScreenshots, + blobPath, + 'w', // write permission + 5 // 5 minutes + ); + + return { + blobPath, + uploadUrl, + expiresIn: 300, // 5 minutes + maxSizeBytes: 5 * 1024 * 1024, // 5MB limit + }; + }); + + // ── Admin: Get screenshot view URL (Phase 1.3) ──────────────────────────── + app.get<{ Params: { id: string } }>('/feedback/:id/screenshot', async req => { + requireAdmin(req); + const productId = getRequestProductId(req); + const fb = await getFeedback(req.params.id, productId); + + if (!fb) throw new NotFoundError('Feedback not found'); + if (!hasScreenshot(fb)) { + throw new NotFoundError('No screenshot attached to this feedback'); + } + + // Generate fresh SAS URL for viewing (15 minutes expiry) + const viewUrl = await generateSasUrl( + BLOB_CONTAINERS.feedbackScreenshots, + fb.screenshotBlobPath!, + 'r', // read permission + 15 // 15 minutes + ); + + return { + url: viewUrl, + contentType: fb.screenshotContentType, + sizeBytes: fb.screenshotSizeBytes, + expiresIn: 900, // 15 minutes + }; + }); + + // ── Admin: Delete screenshot (GDPR/privacy compliance) (Phase 1.3) ─────── + app.delete<{ Params: { id: string } }>('/feedback/:id/screenshot', async (req, reply) => { + requireAdmin(req); + const productId = getRequestProductId(req); + const fb = await getFeedback(req.params.id, productId); + + if (!fb) throw new NotFoundError('Feedback not found'); + if (!hasScreenshot(fb)) { + throw new NotFoundError('No screenshot attached to this feedback'); + } + + // Update feedback to remove screenshot reference + const updated = await updateFeedback(req.params.id, productId, { + screenshotBlobPath: undefined, + screenshotUrl: undefined, + screenshotUrlExpiresAt: undefined, + screenshotContentType: undefined, + screenshotSizeBytes: undefined, + } as unknown as import('./types.js').UpdateFeedbackInput); + + // Note: Actual blob deletion would be handled by lifecycle policy or batch job + // to avoid blocking the request + + reply.status(204); + }); }