Admin 9152cccb0a
Some checks failed
CI / verify (push) Has been cancelled
Sync FieldLens access request approvals
2026-09-23 02:20:39 +00:00

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
}