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:
@@ -0,0 +1,21 @@
|
||||
// Byte formatting for storage figures. Binary units (GiB/TiB) to match what
|
||||
// OCIS reports. Auto-imported by Nuxt (utils/ is scanned by default).
|
||||
|
||||
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
|
||||
// Human-readable size, e.g. 1610612736 → "1.5 GB". Picks the largest unit that
|
||||
// keeps the number readable; trims trailing ".0".
|
||||
export function formatBytes(bytes: number, decimals = 1): string {
|
||||
if (!bytes || bytes < 0) return '0 GB'
|
||||
const i = Math.min(UNITS.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)))
|
||||
const value = bytes / 1024 ** i
|
||||
const fixed = value.toFixed(decimals)
|
||||
return `${fixed.endsWith('.0') ? fixed.slice(0, -2) : fixed} ${UNITS[i]}`
|
||||
}
|
||||
|
||||
// Integer percentage of used vs total, clamped to 0–100. Returns 0 when total
|
||||
// is 0 (unlimited) to avoid NaN in width styles.
|
||||
export function percent(used: number, total: number): number {
|
||||
if (!total || total <= 0) return 0
|
||||
return Math.min(100, Math.max(0, Math.round((used / total) * 100)))
|
||||
}
|
||||
Reference in New Issue
Block a user