Files
selfphoneng/apps/api/src/sso/role.ts
T
iamdoubz b06335586b auth: sign in with LDAP/LDAPS, directory settings and Test (S4)
Spec 12 S4: the login form checks the SelfPhoneNG password first, then the directory (ldapts,
service-account search, the user's own bind on a second connection). Presets for lldap, AD,
FreeIPA and OpenLDAP; Test with an optional test login. A directory account connects by email,
never makes a user; adminFilter sets the role (applyRole shared with OpenID). One Disconnect
route per kind (oidc, ldap). Live test against lldap 0.6.3 over LDAPS.
2026-10-25 18:39:52 -05:00

36 lines
1.3 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
// The role a provider says (12 §1): OpenID adminGroups or the LDAP adminFilter.
import { eq } from 'drizzle-orm';
import type { Db } from '../db/client.ts';
import { users } from '../db/schema.ts';
import { isLastAdmin } from '../routes/users.ts';
import { audit } from '../services/audit.ts';
/** admin null: the role is managed here. Never demotes the last admin with a password. */
export async function applyRole(
db: Db,
log: { info(o: object, m: string): void },
userId: string,
admin: boolean | null,
source: string,
actorIp: string | null,
): Promise<void> {
if (admin === null) return;
const [user] = await db.select().from(users).where(eq(users.id, userId));
const role = admin ? 'admin' : 'user';
if (!user || role === user.role) return;
if (role === 'user' && (await isLastAdmin(db, userId))) {
log.info({ userId }, `kept the last admin with a password, though ${source} says user`);
return;
}
await db.update(users).set({ role }).where(eq(users.id, userId));
await audit(db, {
actorIp: actorIp ?? undefined,
action: 'update',
entityType: 'user',
entityId: userId,
summary: `${user.email} is now ${role}, from ${source}`,
diff: { before: { role: user.role }, after: { role } },
});
}