storage-keys.ts126 lines · main
| 1 | import { Hono } from 'hono'; |
| 2 | import { z } from 'zod'; |
| 3 | |
| 4 | import { env } from '../env.js'; |
| 5 | import { log } from '../lib/logger.js'; |
| 6 | import { |
| 7 | requireProjectAuth, |
| 8 | requireProjectRole, |
| 9 | } from '../middleware/project-auth.js'; |
| 10 | import { audit, hashIp } from '../services/audit.js'; |
| 11 | import { isMinioAdminConfigured } from '../services/minio-admin.js'; |
| 12 | import { |
| 13 | createStorageKey, |
| 14 | listStorageKeys, |
| 15 | revokeStorageKey, |
| 16 | } from '../services/storage-keys.js'; |
| 17 | import type { AppEnv } from '../types/app-env.js'; |
| 18 | import type { User } from '../middleware/session.js'; |
| 19 | |
| 20 | /** |
| 21 | * Customer-facing storage keys — mint / list / revoke bucket-scoped S3 keys for |
| 22 | * a project. Accepts dashboard session OR project CLI key (brk_…), same as |
| 23 | * env/db routes — so `briven storage setup` works after Convex-style setup. |
| 24 | * Secret returned ONCE on create. |
| 25 | */ |
| 26 | export const storageKeysRouter = new Hono<AppEnv>(); |
| 27 | |
| 28 | storageKeysRouter.use('/v1/projects/:id/storage-keys', requireProjectAuth()); |
| 29 | storageKeysRouter.use('/v1/projects/:id/storage-keys/*', requireProjectAuth()); |
| 30 | |
| 31 | const createSchema = z.object({ name: z.string().min(1).max(80) }); |
| 32 | |
| 33 | function ipHashFrom(c: { |
| 34 | req: { header: (n: string) => string | undefined }; |
| 35 | }): string | null { |
| 36 | return hashIp(c.req.header('cf-connecting-ip') ?? c.req.header('x-forwarded-for') ?? null); |
| 37 | } |
| 38 | |
| 39 | function notConfigured(c: { json: (b: unknown, s: number) => Response }) { |
| 40 | return c.json( |
| 41 | { code: 'storage_not_configured', message: 'object storage is not configured on this api' }, |
| 42 | 503, |
| 43 | ); |
| 44 | } |
| 45 | |
| 46 | storageKeysRouter.get( |
| 47 | '/v1/projects/:id/storage-keys', |
| 48 | requireProjectRole('viewer'), |
| 49 | async (c) => { |
| 50 | const projectId = c.req.param('id'); |
| 51 | const keys = await listStorageKeys(projectId); |
| 52 | return c.json({ |
| 53 | keys, |
| 54 | endpoint: env.BRIVEN_MINIO_PUBLIC_ENDPOINT ?? env.BRIVEN_MINIO_ENDPOINT ?? '', |
| 55 | }); |
| 56 | }, |
| 57 | ); |
| 58 | |
| 59 | storageKeysRouter.post( |
| 60 | '/v1/projects/:id/storage-keys', |
| 61 | requireProjectRole('admin'), |
| 62 | async (c) => { |
| 63 | if (!isMinioAdminConfigured()) return notConfigured(c); |
| 64 | const projectId = c.req.param('id'); |
| 65 | const user = c.get('user') as User | null; |
| 66 | |
| 67 | const body = await c.req.json().catch(() => null); |
| 68 | const parsed = createSchema.safeParse(body); |
| 69 | if (!parsed.success) { |
| 70 | return c.json( |
| 71 | { |
| 72 | code: 'validation_failed', |
| 73 | message: 'invalid request body', |
| 74 | issues: parsed.error.issues, |
| 75 | }, |
| 76 | 400, |
| 77 | ); |
| 78 | } |
| 79 | |
| 80 | let created; |
| 81 | try { |
| 82 | created = await createStorageKey({ |
| 83 | projectId, |
| 84 | name: parsed.data.name, |
| 85 | createdBy: user?.id ?? null, |
| 86 | publicEndpoint: env.BRIVEN_MINIO_PUBLIC_ENDPOINT ?? env.BRIVEN_MINIO_ENDPOINT ?? '', |
| 87 | }); |
| 88 | } catch (err) { |
| 89 | const message = err instanceof Error ? err.message : String(err); |
| 90 | log.error('storage_key_mint_failed', { projectId, error: message }); |
| 91 | return c.json( |
| 92 | { code: 'mint_failed', message: `could not mint key — ${message}`.slice(0, 600) }, |
| 93 | 502, |
| 94 | ); |
| 95 | } |
| 96 | await audit({ |
| 97 | actorId: user?.id ?? c.get('apiKeyId') ?? 'api_key', |
| 98 | projectId, |
| 99 | action: 'storage_key.create', |
| 100 | ipHash: ipHashFrom(c), |
| 101 | userAgent: c.req.header('user-agent') ?? null, |
| 102 | metadata: { keyId: created.record.id, name: created.record.name, bucket: created.bucket }, |
| 103 | }); |
| 104 | return c.json(created, 201); |
| 105 | }, |
| 106 | ); |
| 107 | |
| 108 | storageKeysRouter.delete( |
| 109 | '/v1/projects/:id/storage-keys/:keyId', |
| 110 | requireProjectRole('admin'), |
| 111 | async (c) => { |
| 112 | const projectId = c.req.param('id'); |
| 113 | const user = c.get('user') as User | null; |
| 114 | const keyId = c.req.param('keyId'); |
| 115 | await revokeStorageKey(projectId, keyId); |
| 116 | await audit({ |
| 117 | actorId: user?.id ?? c.get('apiKeyId') ?? 'api_key', |
| 118 | projectId, |
| 119 | action: 'storage_key.revoke', |
| 120 | ipHash: ipHashFrom(c), |
| 121 | userAgent: c.req.header('user-agent') ?? null, |
| 122 | metadata: { keyId }, |
| 123 | }); |
| 124 | return c.json({ ok: true }); |
| 125 | }, |
| 126 | ); |