feat(portal): real Security & audit page (+ bundled Storage / per-tenant-roles WIP)
Security & audit (admin) - Audit log: real, tenant-scoped — widened GET /tenants/:slug/audit with q/action/outcome/actorEmail/since/before; UI gains search, outcome + time filters, action chips, cursor pagination, and client-side CSV export. - Security policy: new tenant.securityPolicy (mfaMode, session idle/absolute, allowedCountries, ipAllowlist) + PATCH /tenants/:slug/security-policy (membership-gated, audited). Editable, labelled by enforcement status. - MFA: live enrollment overview via GET /tenants/:slug/mfa-status (Authentik countAuthenticators per member). - SSO apps (Dezky as IdP): real Authentik OIDC provider + application CRUD, scoped to the tenant group. New AuthentikClient methods (provider/app/binding + flow/key/scope discovery), TenantSsoApp schema, TenantSsoService (rollback on partial failure; client secret never stored), GET/POST/DELETE /tenants/:slug/sso-apps. Validated end-to-end against live Authentik. - Deferred: shared-flow MFA/geo/session enforcement (global auth-flow blast radius) — to be done as its own reviewed change. Bundled in-progress work that shares the same files (kept together so the tree stays green): - Storage page: StorageService + GET /tenants/:slug/storage (OCIS-backed), storage.get proxy, storage.vue. - Per-tenant roles: User.tenantRoles + MeProfile.tenantRoles plumbing.
This commit is contained in:
@@ -59,12 +59,20 @@ export class UsersService {
|
||||
if (exists) throw new ConflictException(`User ${dto.authentikSubjectId} already exists`)
|
||||
|
||||
const tenantIds = await this.resolveTenantIds(dto.tenantSlugs ?? [])
|
||||
const role = dto.role ?? 'member'
|
||||
// Seed the per-tenant role for every tenant this user is created into, so
|
||||
// their effective role is explicit from the start rather than relying on
|
||||
// the global-role fallback. Omitted when there are no tenants.
|
||||
const tenantRoles = tenantIds.length
|
||||
? Object.fromEntries(tenantIds.map((id) => [String(id), role]))
|
||||
: undefined
|
||||
return this.userModel.create({
|
||||
authentikSubjectId: dto.authentikSubjectId,
|
||||
email: dto.email,
|
||||
name: dto.name,
|
||||
role: dto.role ?? 'member',
|
||||
role,
|
||||
tenantIds,
|
||||
tenantRoles,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -645,7 +653,11 @@ export class UsersService {
|
||||
.findOneAndUpdate(
|
||||
{ authentikSubjectId: existing.uid },
|
||||
{
|
||||
$set: { email: existing.email },
|
||||
// tenantRoles via $set (not $setOnInsert) so an EXISTING user
|
||||
// invited as admin to this tenant actually becomes admin here,
|
||||
// without disturbing their role in other tenants. The global
|
||||
// `role` stays $setOnInsert as the legacy/first-tenant fallback.
|
||||
$set: { email: existing.email, [`tenantRoles.${tenant._id}`]: 'admin' },
|
||||
$setOnInsert: {
|
||||
name: existing.name || dto.name,
|
||||
role: 'admin',
|
||||
@@ -688,7 +700,8 @@ export class UsersService {
|
||||
.findOneAndUpdate(
|
||||
{ authentikSubjectId: created.uid },
|
||||
{
|
||||
$set: { email: dto.email, name: dto.name },
|
||||
// Per-tenant admin role via $set (see attach branch above).
|
||||
$set: { email: dto.email, name: dto.name, [`tenantRoles.${tenant._id}`]: 'admin' },
|
||||
$setOnInsert: { role: 'admin', active: true, platformAdmin: false },
|
||||
$addToSet: { tenantIds: tenant._id },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user