111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
import { pool } from './db.js'
|
|
|
|
export type AccessStatus = 'pending' | 'approved' | 'rejected'
|
|
export type AppRole = 'fieldlens-operator' | 'fieldlens-supervisor' | 'fieldlens-admin'
|
|
|
|
export type AccessRequest = {
|
|
subject: string
|
|
username: string | null
|
|
email: string | null
|
|
status: AccessStatus
|
|
approvedRole: AppRole | null
|
|
requestedAt: string
|
|
reviewedAt: string | null
|
|
reviewedBy: string | null
|
|
}
|
|
|
|
const APP_ROLES = new Set<AppRole>(['fieldlens-operator', 'fieldlens-supervisor', 'fieldlens-admin'])
|
|
|
|
function role(value: unknown): AppRole | null {
|
|
return typeof value === 'string' && APP_ROLES.has(value as AppRole) ? value as AppRole : null
|
|
}
|
|
|
|
export async function approvedRoleForSubject(subject: string): Promise<AppRole | null> {
|
|
const result = await pool.query<{ approved_role: string | null }>(
|
|
"SELECT approved_role FROM fieldlens_access_requests WHERE subject = $1 AND status = 'approved'",
|
|
[subject],
|
|
)
|
|
return role(result.rows[0]?.approved_role)
|
|
}
|
|
|
|
export async function getAccessRequest(subject: string): Promise<AccessRequest | null> {
|
|
const result = await pool.query<AccessRequest>(`
|
|
SELECT subject, username, email, status,
|
|
approved_role AS "approvedRole",
|
|
requested_at AS "requestedAt",
|
|
reviewed_at AS "reviewedAt",
|
|
reviewed_by AS "reviewedBy"
|
|
FROM fieldlens_access_requests
|
|
WHERE subject = $1
|
|
`, [subject])
|
|
return result.rows[0] ?? null
|
|
}
|
|
|
|
export async function requestAccess(identity: { subject: string; username?: string; email?: string }) {
|
|
const result = await pool.query<AccessRequest>(`
|
|
INSERT INTO fieldlens_access_requests (subject, username, email, status)
|
|
VALUES ($1, $2, $3, 'pending')
|
|
ON CONFLICT (subject) DO UPDATE SET
|
|
username = EXCLUDED.username,
|
|
email = EXCLUDED.email,
|
|
status = CASE
|
|
WHEN fieldlens_access_requests.status = 'approved' THEN 'approved'
|
|
ELSE 'pending'
|
|
END,
|
|
requested_at = CASE
|
|
WHEN fieldlens_access_requests.status = 'approved' THEN fieldlens_access_requests.requested_at
|
|
ELSE now()
|
|
END,
|
|
reviewed_at = CASE
|
|
WHEN fieldlens_access_requests.status = 'approved' THEN fieldlens_access_requests.reviewed_at
|
|
ELSE NULL
|
|
END,
|
|
reviewed_by = CASE
|
|
WHEN fieldlens_access_requests.status = 'approved' THEN fieldlens_access_requests.reviewed_by
|
|
ELSE NULL
|
|
END
|
|
RETURNING subject, username, email, status,
|
|
approved_role AS "approvedRole",
|
|
requested_at AS "requestedAt",
|
|
reviewed_at AS "reviewedAt",
|
|
reviewed_by AS "reviewedBy"
|
|
`, [identity.subject, identity.username ?? null, identity.email ?? null])
|
|
return result.rows[0]!
|
|
}
|
|
|
|
export async function listAccessRequests(status: AccessStatus = 'pending') {
|
|
const result = await pool.query<AccessRequest>(`
|
|
SELECT subject, username, email, status,
|
|
approved_role AS "approvedRole",
|
|
requested_at AS "requestedAt",
|
|
reviewed_at AS "reviewedAt",
|
|
reviewed_by AS "reviewedBy"
|
|
FROM fieldlens_access_requests
|
|
WHERE status = $1
|
|
ORDER BY requested_at ASC
|
|
LIMIT 100
|
|
`, [status])
|
|
return result.rows
|
|
}
|
|
|
|
export async function reviewAccessRequest(
|
|
subject: string,
|
|
decision: 'approved' | 'rejected',
|
|
reviewer: string,
|
|
) {
|
|
const result = await pool.query<AccessRequest>(`
|
|
UPDATE fieldlens_access_requests
|
|
SET status = $2,
|
|
approved_role = CASE WHEN $2 = 'approved' THEN 'fieldlens-operator' ELSE NULL END,
|
|
reviewed_at = now(),
|
|
reviewed_by = $3
|
|
WHERE subject = $1
|
|
RETURNING subject, username, email, status,
|
|
approved_role AS "approvedRole",
|
|
requested_at AS "requestedAt",
|
|
reviewed_at AS "reviewedAt",
|
|
reviewed_by AS "reviewedBy"
|
|
`, [subject, decision, reviewer])
|
|
return result.rows[0] ?? null
|
|
}
|