feat: Azure (Entra ID) login as user login (#16)
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>
This commit is contained in:
56
src/lib/azureAuth.js
Normal file
56
src/lib/azureAuth.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
Reference in New Issue
Block a user