Replaces the client-side PIN login with real authentication against Azure Entra ID via PocketBase's built-in OAuth2 (OIDC) flow. Every Azure user is auto-provisioned as a team_member on first login. - pb_migrations: team_members becomes a PocketBase auth collection (OIDC provider configured from env); all collections require @request.auth.id - pb_hooks: provisioning of role (ENTRA_ADMIN_EMAILS allow-list) and enrollment_status, with admin-role re-sync on every login - frontend: "Sign in with Microsoft" login, pb.authStore-based session, TeamManager manages roster/roles (no PIN/manual create) - infra: Entra env wiring + pb_hooks mount for dev (Labs) and prod - src/lib/azureAuth.js + tests for the canonical role/allow-list logic - docs/auth-spec.md Knowledge base, generated tests and micro-learnings are left untouched. Existing PIN users are dropped (no migration required, per sign-off). Closes #16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
1.9 KiB
JavaScript
57 lines
1.9 KiB
JavaScript
/**
|
|
* Azure (Entra ID) authentication helpers.
|
|
*
|
|
* Canonical, unit-tested home for the small pieces of auth logic shared by the
|
|
* frontend. The PocketBase hook `pb_hooks/team_members.pb.js` re-implements
|
|
* `resolveRole` / `parseAdminEmails` in the JSVM runtime (it cannot import this
|
|
* module) — keep the two in sync.
|
|
*
|
|
* Login itself goes through PocketBase's built-in OAuth2 flow:
|
|
* pb.collection('team_members').authWithOAuth2({ provider: OIDC_PROVIDER })
|
|
* The OIDC provider ("oidc") is configured against Entra in migration
|
|
* 1781000000_team_members_to_auth.js.
|
|
*/
|
|
|
|
/** Provider name registered on the `team_members` auth collection. */
|
|
export const OIDC_PROVIDER = 'oidc';
|
|
|
|
/**
|
|
* Parse the ENTRA_ADMIN_EMAILS-style comma-separated allow-list into a
|
|
* normalised (lower-cased, trimmed, de-duplicated, empty-free) array.
|
|
* @param {string} [csv]
|
|
* @returns {string[]}
|
|
*/
|
|
export function parseAdminEmails(csv) {
|
|
if (!csv) return [];
|
|
const seen = new Set();
|
|
for (const part of String(csv).toLowerCase().split(',')) {
|
|
const email = part.trim();
|
|
if (email) seen.add(email);
|
|
}
|
|
return [...seen];
|
|
}
|
|
|
|
/**
|
|
* Resolve a user's role from their e-mail and the admin allow-list.
|
|
* @param {string} email
|
|
* @param {string} [adminEmailsCsv]
|
|
* @returns {'admin'|'user'}
|
|
*/
|
|
export function resolveRole(email, adminEmailsCsv) {
|
|
const allow = parseAdminEmails(adminEmailsCsv);
|
|
return allow.includes(String(email || '').trim().toLowerCase()) ? 'admin' : 'user';
|
|
}
|
|
|
|
/**
|
|
* Derive a display name from OIDC claims, falling back to the e-mail prefix.
|
|
* @param {{ name?: string }} [claims]
|
|
* @param {string} [email]
|
|
* @returns {string}
|
|
*/
|
|
export function deriveName(claims, email) {
|
|
const fromClaim = claims && typeof claims.name === 'string' ? claims.name.trim() : '';
|
|
if (fromClaim) return fromClaim;
|
|
if (email && email.includes('@')) return email.split('@')[0];
|
|
return 'Onbekend';
|
|
}
|