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
@@ -0,0 +1,13 @@
|
||||
CREATE TYPE "public"."auth_token_purpose" AS ENUM('magic_link', 'password_reset');--> statement-breakpoint
|
||||
CREATE TABLE "auth_tokens" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"purpose" "auth_token_purpose" NOT NULL,
|
||||
"expires_at" timestamp with time zone NOT NULL,
|
||||
"used_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "corrective_actions" ADD COLUMN "reminder_sent_on" date;--> statement-breakpoint
|
||||
ALTER TABLE "auth_tokens" ADD CONSTRAINT "auth_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "auth_tokens_user_idx" ON "auth_tokens" USING btree ("user_id");
|
||||
File diff suppressed because it is too large.
Load diff
@@ -8,6 +8,13 @@
|
||||
"when": 1791306770507,
|
||||
"tag": "0000_initial",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1791309275942,
|
||||
"tag": "0001_workflow_and_email",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export const rcaMethodEnum = pgEnum('rca_method', vocab.rcaMethods)
|
||||
export const rootCauseCategoryEnum = pgEnum('root_cause_category', vocab.rootCauseCategories)
|
||||
export const actionKindEnum = pgEnum('action_kind', vocab.actionKinds)
|
||||
export const actionStatusEnum = pgEnum('action_status', vocab.actionStatuses)
|
||||
export const authTokenPurposeEnum = pgEnum('auth_token_purpose', ['magic_link', 'password_reset'])
|
||||
|
||||
const id = () => uuid().primaryKey().defaultRandom()
|
||||
const timestamptz = () => timestamp({ withTimezone: true })
|
||||
@@ -62,6 +63,23 @@ export const sessions = pgTable(
|
||||
(t) => [index('sessions_user_idx').on(t.userId)],
|
||||
)
|
||||
|
||||
/** Single-use links sent by email (magic sign-in, password reset/invite). */
|
||||
export const authTokens = pgTable(
|
||||
'auth_tokens',
|
||||
{
|
||||
/** SHA-256 of the token sent in the link. */
|
||||
id: text().primaryKey(),
|
||||
userId: uuid()
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
purpose: authTokenPurposeEnum().notNull(),
|
||||
expiresAt: timestamptz().notNull(),
|
||||
usedAt: timestamptz(),
|
||||
createdAt: createdAt(),
|
||||
},
|
||||
(t) => [index('auth_tokens_user_idx').on(t.userId)],
|
||||
)
|
||||
|
||||
export const sites = pgTable(
|
||||
'sites',
|
||||
{
|
||||
@@ -230,6 +248,8 @@ export const correctiveActions = pgTable(
|
||||
completedAt: timestamptz(),
|
||||
verifiedById: uuid().references(() => users.id, { onDelete: 'set null' }),
|
||||
verifiedAt: timestamptz(),
|
||||
/** Date the last due/overdue reminder was emailed, so it goes out at most once a day. */
|
||||
reminderSentOn: date({ mode: 'string' }),
|
||||
createdAt: createdAt(),
|
||||
updatedAt: updatedAt(),
|
||||
},
|
||||
|
||||
+18
-3
@@ -2,7 +2,7 @@
|
||||
import { hashPassword } from '../auth/password.ts'
|
||||
import { loadConfig } from '../config.ts'
|
||||
import { openDatabase } from './client.ts'
|
||||
import { areas, incidentCounters, incidents, organizations, sites, users } from './schema.ts'
|
||||
import { auditLog, areas, incidentCounters, incidents, organizations, sites, users } from './schema.ts'
|
||||
|
||||
const DEMO_PASSWORD = 'safetylog-demo'
|
||||
|
||||
@@ -49,8 +49,10 @@ try {
|
||||
{ title: 'Wet floor near employee entrance', type: 'hazard', severity: 'medium', status: 'actions', building: 'Building A', locationDetail: 'Ground floor lobby', reporter: 'reporter@example.com', days: 6 },
|
||||
{ title: 'Forklift reversed into pallet stack', type: 'near_miss', severity: 'high', status: 'triage', building: 'Warehouse 04', locationDetail: 'Loading bay C', reporter: 'admin@example.com', days: 5 },
|
||||
] as const
|
||||
await tx.insert(incidents).values(
|
||||
demo.map((item, index) => ({
|
||||
const created = await tx
|
||||
.insert(incidents)
|
||||
.values(
|
||||
demo.map((item, index) => ({
|
||||
organizationId: org.id,
|
||||
number: index + 1,
|
||||
title: item.title,
|
||||
@@ -58,10 +60,23 @@ try {
|
||||
severity: item.severity,
|
||||
status: item.status,
|
||||
occurredAt: daysAgo(item.days),
|
||||
createdAt: new Date(daysAgo(item.days).getTime() + 2 * 3_600_000),
|
||||
...areaIds[item.building],
|
||||
locationDetail: item.locationDetail,
|
||||
reporterId: person(item.reporter),
|
||||
closedAt: item.status === 'closed' ? daysAgo(item.days - 3) : null,
|
||||
})),
|
||||
)
|
||||
.returning()
|
||||
await tx.insert(auditLog).values(
|
||||
created.map((incident) => ({
|
||||
organizationId: org.id,
|
||||
actorId: incident.reporterId,
|
||||
entityType: 'incident',
|
||||
entityId: incident.id,
|
||||
action: 'create',
|
||||
changes: { number: incident.number, title: incident.title },
|
||||
createdAt: incident.createdAt,
|
||||
})),
|
||||
)
|
||||
await tx.insert(incidentCounters).values({ organizationId: org.id, lastNumber: demo.length })
|
||||
|
||||
Reference in new issue
Block a user