feat(admin): serve response media from standalone API

This commit is contained in:
jjbrunton committed 2026-08-28 14:54:07 +01:00
1 parent be20391788
commit 2dad7aa352
9 files changed
+128 -31

No files matched your search

+20
View File
@@ -131,6 +131,7 @@ export interface AdminRepository {
}>>;
users(principal: AdminPrincipal, userId?: string): Promise<Record<string, unknown>[]>;
authorizeMedia(principal: AdminPrincipal, mediaId: string): Promise<void>;
responseMedia(principal: AdminPrincipal, responseId: string): Promise<{ storageKey: string; mimeType: string }>;
decryptMessage(principal: AdminPrincipal, messageId: string): Promise<{ content: string | null }>;
decryptMedia(principal: AdminPrincipal, messageId: string): Promise<{ bytes: Buffer; mimeType: string }>;
close(): Promise<void>;
@@ -287,6 +288,25 @@ export class PostgresAdminRepository implements AdminRepository {
if (!result.rows[0]) throw new AdminError('media_not_found', 'Media not found', 404);
}
async responseMedia(principal: AdminPrincipal, responseId: string): Promise<{ storageKey: string; mimeType: string }> {
requirePermission(principal, 'view_responses');
const result = await this.pool.query<{ storage_key: string; mime_type: string }>(
`select mo.storage_key,mo.mime_type
from responses r
join media_objects mo
on r.response_data->>'media_path'='media:'||mo.id::text
and mo.kind='response'
and mo.owner_id=r.user_id
and mo.couple_id=r.couple_id
and (mo.question_id is null or mo.question_id=r.question_id)
where r.id=$1 and mo.deleted_at is null and (mo.expires_at is null or mo.expires_at>now())`,
[responseId],
);
const row = result.rows[0];
if (!row) throw new AdminError('response_media_not_found', 'Response media not found', 404);
return { storageKey: row.storage_key, mimeType: row.mime_type };
}
private async messageForDecryption(messageId: string) {
const result = await this.pool.query<{
content: string | null; encrypted_content: string | null; encryption_iv: string | null;
+20
View File
@@ -97,6 +97,26 @@ export function registerAdminRoutes(app: App, repository: AdminRepository, auth?
return result.ok ? c.json(result.value, 200, { 'cache-control': 'private, no-store' }) : c.json(failure(result.cause.code, result.cause.message), result.cause.status);
});
app.get('/v1/admin/responses/:responseId/media', async (c) => {
const parsed = z.string().uuid().safeParse(c.req.param('responseId'));
if (!parsed.success) return c.json(failure('invalid_response_id', 'A valid response ID is required'), 400);
if (!mediaStorage) return c.json(failure('media_unavailable', 'Media storage is unavailable'), 503);
const result = await execute(async () => {
const media = await repository.responseMedia(await repository.principal(c.get('identity').id), parsed.data);
const bytes = await mediaStorage.read(media.storageKey).catch(() => {
throw new AdminError('response_media_not_found', 'Response media not found', 404);
});
return { ...media, bytes };
});
if (!result.ok) return c.json(failure(result.cause.code, result.cause.message), result.cause.status);
return c.body(new Uint8Array(result.value.bytes), 200, {
'content-type': result.value.mimeType,
'content-length': String(result.value.bytes.byteLength),
'cache-control': 'private, no-store',
'x-content-type-options': 'nosniff',
});
});
app.post('/v1/admin/users/:userId/gift-premium', async (c) => {
const parsed = z.object({ days: z.number().int().min(1).max(3650).nullable(), reason: z.string().trim().max(500).optional() }).strict()
.safeParse(await c.req.json().catch(() => ({})));