mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
feat(admin): serve response media from standalone API
This commit is contained in:
1 parent
be20391788
commit
2dad7aa352
9 files changed
+128
-31
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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(() => ({})));
|
||||
|
||||
Reference in new issue
Block a user