feat: implement permissions and actions management
- 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.
This commit is contained in:
1 parent
708da6b9f7
commit
3f6dff5f24
94 files changed
+7213
-643
No files matched your search
+41
-5
@@ -1,6 +1,12 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { createApp } from '../app.ts'
|
||||
import { loadConfig } from '../config.ts'
|
||||
import { openDatabase } from '../db/client.ts'
|
||||
import { MemoryMailer } from '../mail.ts'
|
||||
import { Notifier } from '../notifications.ts'
|
||||
import { FileStorage } from '../storage.ts'
|
||||
|
||||
type App = ReturnType<typeof createApp>
|
||||
|
||||
@@ -15,12 +21,16 @@ export class Client {
|
||||
|
||||
async request(method: string, path: string, body?: unknown) {
|
||||
const headers: Record<string, string> = {}
|
||||
if (body !== undefined) headers['content-type'] = 'application/json'
|
||||
const isForm = body instanceof FormData
|
||||
if (body !== undefined && !isForm) headers['content-type'] = 'application/json'
|
||||
if (method !== 'GET') headers.origin = 'http://safety.test' // browsers send Origin on every non-GET request
|
||||
if (this.cookie) headers.cookie = this.cookie
|
||||
const res = await this.app.request(path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) })
|
||||
const payload = body === undefined ? undefined : isForm ? body : JSON.stringify(body)
|
||||
const res = await this.app.request(path, { method, headers, body: payload })
|
||||
const setCookie = res.headers.get('set-cookie')
|
||||
if (setCookie) this.cookie = /max-age=0|expires=thu, 01 jan 1970/i.test(setCookie) ? undefined : setCookie.split(';')[0]
|
||||
const text = await res.text()
|
||||
const text = res.headers.get('content-type')?.includes('json') || res.status >= 400 ? await res.text() : ''
|
||||
if (!text && res.ok && res.status !== 204) return { status: res.status, body: Buffer.from(await res.arrayBuffer()), headers: res.headers }
|
||||
// oxlint-disable-next-line typescript/no-explicit-any -- tests read arbitrary JSON shapes
|
||||
const json: any = text ? JSON.parse(text) : null
|
||||
return { status: res.status, body: json, headers: res.headers }
|
||||
@@ -28,7 +38,9 @@ export class Client {
|
||||
|
||||
get = (path: string) => this.request('GET', path)
|
||||
post = (path: string, body?: unknown) => this.request('POST', path, body ?? {})
|
||||
put = (path: string, body: unknown) => this.request('PUT', path, body)
|
||||
patch = (path: string, body: unknown) => this.request('PATCH', path, body)
|
||||
delete = (path: string) => this.request('DELETE', path)
|
||||
}
|
||||
|
||||
export const ADMIN = { organizationName: 'Northstar Co.', name: 'Avery Admin', email: 'admin@example.com', password: 'correct horse battery' }
|
||||
@@ -36,7 +48,11 @@ export const ADMIN = { organizationName: 'Northstar Co.', name: 'Avery Admin', e
|
||||
/** Fresh in-memory database and app, with the first-run setup already done and the admin signed in. */
|
||||
export async function createTestContext() {
|
||||
const database = await openDatabase('memory://')
|
||||
const app = createApp({ db: database.db, config: { ...loadConfig({}), databaseUrl: 'memory://' } })
|
||||
const uploadDir = await mkdtemp(join(tmpdir(), 'safetylog-test-'))
|
||||
const config = { ...loadConfig({ PUBLIC_URL: 'http://safety.test' }), databaseUrl: 'memory://', uploadDir, maxUploadBytes: 1024 * 1024 }
|
||||
const mailer = new MemoryMailer()
|
||||
const notifier = new Notifier(database.db, mailer, config)
|
||||
const app = createApp({ db: database.db, config, notifier, storage: new FileStorage(uploadDir) })
|
||||
const admin = new Client(app)
|
||||
const setup = await admin.post('/api/setup', ADMIN)
|
||||
if (setup.status !== 201) throw new Error(`setup failed: ${JSON.stringify(setup.body)}`)
|
||||
@@ -55,7 +71,27 @@ export async function createTestContext() {
|
||||
return res.body.site as { id: string; name: string }
|
||||
}
|
||||
|
||||
return { app, db: database.db, admin, anonymous: () => new Client(app), createUser, createSite, close: database.close }
|
||||
async function close() {
|
||||
await notifier.flush()
|
||||
await database.close()
|
||||
await rm(uploadDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
return { app, db: database.db, admin, mailer, notifier, config, anonymous: () => new Client(app), createUser, createSite, close }
|
||||
}
|
||||
|
||||
export const hoursAgo = (hours: number) => new Date(Date.now() - hours * 3_600_000).toISOString()
|
||||
|
||||
/** Pulls the token out of the link in an email body. */
|
||||
export function tokenFrom(text: string | undefined): string {
|
||||
const match = /token=([\w-]+)/.exec(text ?? '')
|
||||
if (!match) throw new Error(`No token in: ${text}`)
|
||||
return match[1]
|
||||
}
|
||||
|
||||
export const daysFromToday = (days: number) => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + days)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
}
|
||||
Reference in new issue
Block a user