- Add permissions logic for status options in corrective actions. - Create tests for permissions functionality. - Implement queries for user authentication and actions management. - Develop Actions page to display and manage corrective actions. - Create authentication pages for login, setup, and password reset. - Add IncidentDetail page to view incident details and associated actions. - Implement Settings page for user management and password changes.
49 lines
2.3 KiB
TypeScript
49 lines
2.3 KiB
TypeScript
import { and, eq } from 'drizzle-orm'
|
|
import { Hono } from 'hono'
|
|
import { z } from 'zod'
|
|
import { hasRole } from '../../shared/enums.ts'
|
|
import { requireAuth } from '../auth/middleware.ts'
|
|
import type { SessionUser } from '../auth/sessions.ts'
|
|
import type { Db } from '../db/client.ts'
|
|
import { attachments, incidents } from '../db/schema.ts'
|
|
import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts'
|
|
import { auditIncident, visibleTo } from '../lib/incidents.ts'
|
|
|
|
const idParam = z.object({ id: z.uuid() })
|
|
|
|
async function findAttachment(db: Db, user: SessionUser, id: string) {
|
|
const [row] = await db
|
|
.select({ file: attachments })
|
|
.from(attachments)
|
|
.innerJoin(incidents, eq(incidents.id, attachments.incidentId))
|
|
.where(and(eq(attachments.id, id), visibleTo(db, user)))
|
|
if (!row) throw fail(404, 'Attachment not found')
|
|
return row.file
|
|
}
|
|
|
|
export const attachmentRoutes = new Hono<AppEnv>()
|
|
.get('/:id', requireAuth, validate('param', idParam), async (c) => {
|
|
const file = await findAttachment(c.get('db'), currentUser(c), c.req.valid('param').id)
|
|
const data = await c.get('storage').read(file.storageKey)
|
|
const disposition = file.contentType.startsWith('image/') ? 'inline' : 'attachment'
|
|
return c.body(new Uint8Array(data), 200, {
|
|
'Content-Type': file.contentType,
|
|
'Content-Length': String(data.byteLength),
|
|
'Content-Disposition': `${disposition}; filename*=UTF-8''${encodeURIComponent(file.fileName)}`,
|
|
// Uploaded content never runs scripts, even if a file slipped past type detection
|
|
'Content-Security-Policy': "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox",
|
|
'Cache-Control': 'private, max-age=3600',
|
|
})
|
|
})
|
|
|
|
.delete('/:id', requireAuth, validate('param', idParam), async (c) => {
|
|
const me = currentUser(c)
|
|
const db = c.get('db')
|
|
const file = await findAttachment(db, me, c.req.valid('param').id)
|
|
if (file.uploadedById !== me.id && !hasRole(me.role, 'supervisor')) throw fail(403, 'Only the uploader or a supervisor can remove this file')
|
|
await db.delete(attachments).where(eq(attachments.id, file.id))
|
|
await c.get('storage').remove(file.storageKey)
|
|
await auditIncident(db, me, me.organizationId, file.incidentId, 'attachment.remove', { fileName: file.fileName })
|
|
return c.body(null, 204)
|
|
})
|