@portaidentity/server 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/applications/cache.d.ts +64 -0
- package/dist/applications/cache.d.ts.map +1 -0
- package/dist/applications/cache.js +161 -0
- package/dist/applications/cache.js.map +1 -0
- package/dist/applications/errors.d.ts +25 -0
- package/dist/applications/errors.d.ts.map +1 -0
- package/dist/applications/errors.js +31 -0
- package/dist/applications/errors.js.map +1 -0
- package/dist/applications/index.d.ts +17 -0
- package/dist/applications/index.d.ts.map +1 -0
- package/dist/applications/index.js +19 -0
- package/dist/applications/index.js.map +1 -0
- package/dist/applications/repository.d.ts +184 -0
- package/dist/applications/repository.d.ts.map +1 -0
- package/dist/applications/repository.js +367 -0
- package/dist/applications/repository.js.map +1 -0
- package/dist/applications/service.d.ts +160 -0
- package/dist/applications/service.d.ts.map +1 -0
- package/dist/applications/service.js +388 -0
- package/dist/applications/service.js.map +1 -0
- package/dist/applications/slugs.d.ts +61 -0
- package/dist/applications/slugs.d.ts.map +1 -0
- package/dist/applications/slugs.js +115 -0
- package/dist/applications/slugs.js.map +1 -0
- package/dist/applications/types.d.ts +129 -0
- package/dist/applications/types.d.ts.map +1 -0
- package/dist/applications/types.js +62 -0
- package/dist/applications/types.js.map +1 -0
- package/dist/auth/csrf.d.ts +71 -0
- package/dist/auth/csrf.d.ts.map +1 -0
- package/dist/auth/csrf.js +112 -0
- package/dist/auth/csrf.js.map +1 -0
- package/dist/auth/email-renderer.d.ts +37 -0
- package/dist/auth/email-renderer.d.ts.map +1 -0
- package/dist/auth/email-renderer.js +116 -0
- package/dist/auth/email-renderer.js.map +1 -0
- package/dist/auth/email-service.d.ts +154 -0
- package/dist/auth/email-service.d.ts.map +1 -0
- package/dist/auth/email-service.js +415 -0
- package/dist/auth/email-service.js.map +1 -0
- package/dist/auth/email-transport.d.ts +65 -0
- package/dist/auth/email-transport.d.ts.map +1 -0
- package/dist/auth/email-transport.js +82 -0
- package/dist/auth/email-transport.js.map +1 -0
- package/dist/auth/i18n.d.ts +81 -0
- package/dist/auth/i18n.d.ts.map +1 -0
- package/dist/auth/i18n.js +395 -0
- package/dist/auth/i18n.js.map +1 -0
- package/dist/auth/index.d.ts +28 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +31 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/interaction-authority.d.ts +39 -0
- package/dist/auth/interaction-authority.d.ts.map +1 -0
- package/dist/auth/interaction-authority.js +30 -0
- package/dist/auth/interaction-authority.js.map +1 -0
- package/dist/auth/magic-link-session.d.ts +81 -0
- package/dist/auth/magic-link-session.d.ts.map +1 -0
- package/dist/auth/magic-link-session.js +183 -0
- package/dist/auth/magic-link-session.js.map +1 -0
- package/dist/auth/rate-limiter.d.ts +143 -0
- package/dist/auth/rate-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limiter.js +220 -0
- package/dist/auth/rate-limiter.js.map +1 -0
- package/dist/auth/recovery-crypto.d.ts +34 -0
- package/dist/auth/recovery-crypto.d.ts.map +1 -0
- package/dist/auth/recovery-crypto.js +103 -0
- package/dist/auth/recovery-crypto.js.map +1 -0
- package/dist/auth/recovery-job-processor.d.ts +24 -0
- package/dist/auth/recovery-job-processor.d.ts.map +1 -0
- package/dist/auth/recovery-job-processor.js +123 -0
- package/dist/auth/recovery-job-processor.js.map +1 -0
- package/dist/auth/recovery-job-repository.d.ts +219 -0
- package/dist/auth/recovery-job-repository.d.ts.map +1 -0
- package/dist/auth/recovery-job-repository.js +340 -0
- package/dist/auth/recovery-job-repository.js.map +1 -0
- package/dist/auth/recovery-service.d.ts +27 -0
- package/dist/auth/recovery-service.d.ts.map +1 -0
- package/dist/auth/recovery-service.js +53 -0
- package/dist/auth/recovery-service.js.map +1 -0
- package/dist/auth/recovery-worker.d.ts +158 -0
- package/dist/auth/recovery-worker.d.ts.map +1 -0
- package/dist/auth/recovery-worker.js +306 -0
- package/dist/auth/recovery-worker.js.map +1 -0
- package/dist/auth/template-engine.d.ts +134 -0
- package/dist/auth/template-engine.d.ts.map +1 -0
- package/dist/auth/template-engine.js +225 -0
- package/dist/auth/template-engine.js.map +1 -0
- package/dist/auth/token-repository.d.ts +238 -0
- package/dist/auth/token-repository.d.ts.map +1 -0
- package/dist/auth/token-repository.js +475 -0
- package/dist/auth/token-repository.js.map +1 -0
- package/dist/auth/tokens.d.ts +45 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +51 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/cli/bootstrap.d.ts +55 -0
- package/dist/cli/bootstrap.d.ts.map +1 -0
- package/dist/cli/bootstrap.js +90 -0
- package/dist/cli/bootstrap.js.map +1 -0
- package/dist/cli/commands/health.d.ts +20 -0
- package/dist/cli/commands/health.d.ts.map +1 -0
- package/dist/cli/commands/health.js +80 -0
- package/dist/cli/commands/health.js.map +1 -0
- package/dist/cli/commands/init.d.ts +48 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +349 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/migrate.d.ts +19 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -0
- package/dist/cli/commands/migrate.js +88 -0
- package/dist/cli/commands/migrate.js.map +1 -0
- package/dist/cli/commands/seed.d.ts +19 -0
- package/dist/cli/commands/seed.d.ts.map +1 -0
- package/dist/cli/commands/seed.js +71 -0
- package/dist/cli/commands/seed.js.map +1 -0
- package/dist/cli/commands/user-2fa.d.ts +29 -0
- package/dist/cli/commands/user-2fa.d.ts.map +1 -0
- package/dist/cli/commands/user-2fa.js +113 -0
- package/dist/cli/commands/user-2fa.js.map +1 -0
- package/dist/cli/error-handler.d.ts +31 -0
- package/dist/cli/error-handler.d.ts.map +1 -0
- package/dist/cli/error-handler.js +65 -0
- package/dist/cli/error-handler.js.map +1 -0
- package/dist/cli/index.d.ts +48 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +99 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +94 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +124 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/prompt.d.ts +48 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/prompt.js +96 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/clients/cache.d.ts +63 -0
- package/dist/clients/cache.d.ts.map +1 -0
- package/dist/clients/cache.js +160 -0
- package/dist/clients/cache.js.map +1 -0
- package/dist/clients/crypto.d.ts +69 -0
- package/dist/clients/crypto.d.ts.map +1 -0
- package/dist/clients/crypto.js +83 -0
- package/dist/clients/crypto.js.map +1 -0
- package/dist/clients/errors.d.ts +27 -0
- package/dist/clients/errors.d.ts.map +1 -0
- package/dist/clients/errors.js +33 -0
- package/dist/clients/errors.js.map +1 -0
- package/dist/clients/index.d.ts +19 -0
- package/dist/clients/index.d.ts.map +1 -0
- package/dist/clients/index.js +23 -0
- package/dist/clients/index.js.map +1 -0
- package/dist/clients/repository.d.ts +161 -0
- package/dist/clients/repository.d.ts.map +1 -0
- package/dist/clients/repository.js +318 -0
- package/dist/clients/repository.js.map +1 -0
- package/dist/clients/resolve-login-methods.d.ts +74 -0
- package/dist/clients/resolve-login-methods.d.ts.map +1 -0
- package/dist/clients/resolve-login-methods.js +68 -0
- package/dist/clients/resolve-login-methods.js.map +1 -0
- package/dist/clients/secret-repository.d.ts +109 -0
- package/dist/clients/secret-repository.d.ts.map +1 -0
- package/dist/clients/secret-repository.js +166 -0
- package/dist/clients/secret-repository.js.map +1 -0
- package/dist/clients/secret-service.d.ts +87 -0
- package/dist/clients/secret-service.d.ts.map +1 -0
- package/dist/clients/secret-service.js +204 -0
- package/dist/clients/secret-service.js.map +1 -0
- package/dist/clients/service.d.ts +158 -0
- package/dist/clients/service.d.ts.map +1 -0
- package/dist/clients/service.js +523 -0
- package/dist/clients/service.js.map +1 -0
- package/dist/clients/types.d.ts +214 -0
- package/dist/clients/types.d.ts.map +1 -0
- package/dist/clients/types.js +77 -0
- package/dist/clients/types.js.map +1 -0
- package/dist/clients/validators.d.ts +87 -0
- package/dist/clients/validators.d.ts.map +1 -0
- package/dist/clients/validators.js +164 -0
- package/dist/clients/validators.js.map +1 -0
- package/dist/config/index.d.ts +23 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +52 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +35 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +167 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/custom-claims/cache.d.ts +48 -0
- package/dist/custom-claims/cache.d.ts.map +1 -0
- package/dist/custom-claims/cache.js +122 -0
- package/dist/custom-claims/cache.js.map +1 -0
- package/dist/custom-claims/errors.d.ts +25 -0
- package/dist/custom-claims/errors.d.ts.map +1 -0
- package/dist/custom-claims/errors.js +31 -0
- package/dist/custom-claims/errors.js.map +1 -0
- package/dist/custom-claims/index.d.ts +14 -0
- package/dist/custom-claims/index.d.ts.map +1 -0
- package/dist/custom-claims/index.js +17 -0
- package/dist/custom-claims/index.js.map +1 -0
- package/dist/custom-claims/repository.d.ts +147 -0
- package/dist/custom-claims/repository.d.ts.map +1 -0
- package/dist/custom-claims/repository.js +338 -0
- package/dist/custom-claims/repository.js.map +1 -0
- package/dist/custom-claims/service.d.ts +129 -0
- package/dist/custom-claims/service.d.ts.map +1 -0
- package/dist/custom-claims/service.js +296 -0
- package/dist/custom-claims/service.js.map +1 -0
- package/dist/custom-claims/types.d.ts +132 -0
- package/dist/custom-claims/types.d.ts.map +1 -0
- package/dist/custom-claims/types.js +61 -0
- package/dist/custom-claims/types.js.map +1 -0
- package/dist/custom-claims/validators.d.ts +61 -0
- package/dist/custom-claims/validators.d.ts.map +1 -0
- package/dist/custom-claims/validators.js +136 -0
- package/dist/custom-claims/validators.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/admin-permissions.d.ts +147 -0
- package/dist/lib/admin-permissions.d.ts.map +1 -0
- package/dist/lib/admin-permissions.js +261 -0
- package/dist/lib/admin-permissions.js.map +1 -0
- package/dist/lib/audit-log.d.ts +45 -0
- package/dist/lib/audit-log.d.ts.map +1 -0
- package/dist/lib/audit-log.js +79 -0
- package/dist/lib/audit-log.js.map +1 -0
- package/dist/lib/branding-assets.d.ts +47 -0
- package/dist/lib/branding-assets.d.ts.map +1 -0
- package/dist/lib/branding-assets.js +93 -0
- package/dist/lib/branding-assets.js.map +1 -0
- package/dist/lib/bulk-operations.d.ts +54 -0
- package/dist/lib/bulk-operations.d.ts.map +1 -0
- package/dist/lib/bulk-operations.js +201 -0
- package/dist/lib/bulk-operations.js.map +1 -0
- package/dist/lib/cursor.d.ts +110 -0
- package/dist/lib/cursor.d.ts.map +1 -0
- package/dist/lib/cursor.js +115 -0
- package/dist/lib/cursor.js.map +1 -0
- package/dist/lib/data-export.d.ts +67 -0
- package/dist/lib/data-export.d.ts.map +1 -0
- package/dist/lib/data-export.js +272 -0
- package/dist/lib/data-export.js.map +1 -0
- package/dist/lib/data-import-plan.d.ts +50 -0
- package/dist/lib/data-import-plan.d.ts.map +1 -0
- package/dist/lib/data-import-plan.js +235 -0
- package/dist/lib/data-import-plan.js.map +1 -0
- package/dist/lib/data-import.d.ts +276 -0
- package/dist/lib/data-import.d.ts.map +1 -0
- package/dist/lib/data-import.js +1694 -0
- package/dist/lib/data-import.js.map +1 -0
- package/dist/lib/database.d.ts +22 -0
- package/dist/lib/database.d.ts.map +1 -0
- package/dist/lib/database.js +103 -0
- package/dist/lib/database.js.map +1 -0
- package/dist/lib/entity-history.d.ts +52 -0
- package/dist/lib/entity-history.d.ts.map +1 -0
- package/dist/lib/entity-history.js +121 -0
- package/dist/lib/entity-history.js.map +1 -0
- package/dist/lib/etag.d.ts +92 -0
- package/dist/lib/etag.d.ts.map +1 -0
- package/dist/lib/etag.js +155 -0
- package/dist/lib/etag.js.map +1 -0
- package/dist/lib/image-validator.d.ts +44 -0
- package/dist/lib/image-validator.d.ts.map +1 -0
- package/dist/lib/image-validator.js +204 -0
- package/dist/lib/image-validator.js.map +1 -0
- package/dist/lib/logger.d.ts +15 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +84 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/migrator.d.ts +14 -0
- package/dist/lib/migrator.d.ts.map +1 -0
- package/dist/lib/migrator.js +34 -0
- package/dist/lib/migrator.js.map +1 -0
- package/dist/lib/redis.d.ts +5 -0
- package/dist/lib/redis.d.ts.map +1 -0
- package/dist/lib/redis.js +29 -0
- package/dist/lib/redis.js.map +1 -0
- package/dist/lib/runtime-paths.d.ts +38 -0
- package/dist/lib/runtime-paths.d.ts.map +1 -0
- package/dist/lib/runtime-paths.js +50 -0
- package/dist/lib/runtime-paths.js.map +1 -0
- package/dist/lib/session-tracking.d.ts +81 -0
- package/dist/lib/session-tracking.d.ts.map +1 -0
- package/dist/lib/session-tracking.js +145 -0
- package/dist/lib/session-tracking.js.map +1 -0
- package/dist/lib/signing-key-crypto.d.ts +53 -0
- package/dist/lib/signing-key-crypto.d.ts.map +1 -0
- package/dist/lib/signing-key-crypto.js +108 -0
- package/dist/lib/signing-key-crypto.js.map +1 -0
- package/dist/lib/signing-keys.d.ts +141 -0
- package/dist/lib/signing-keys.d.ts.map +1 -0
- package/dist/lib/signing-keys.js +206 -0
- package/dist/lib/signing-keys.js.map +1 -0
- package/dist/lib/stats.d.ts +83 -0
- package/dist/lib/stats.d.ts.map +1 -0
- package/dist/lib/stats.js +187 -0
- package/dist/lib/stats.js.map +1 -0
- package/dist/lib/super-admin-protection.d.ts +69 -0
- package/dist/lib/super-admin-protection.d.ts.map +1 -0
- package/dist/lib/super-admin-protection.js +102 -0
- package/dist/lib/super-admin-protection.js.map +1 -0
- package/dist/lib/system-config.d.ts +79 -0
- package/dist/lib/system-config.d.ts.map +1 -0
- package/dist/lib/system-config.js +159 -0
- package/dist/lib/system-config.js.map +1 -0
- package/dist/middleware/admin-auth.d.ts +100 -0
- package/dist/middleware/admin-auth.d.ts.map +1 -0
- package/dist/middleware/admin-auth.js +241 -0
- package/dist/middleware/admin-auth.js.map +1 -0
- package/dist/middleware/admin-cors.d.ts +41 -0
- package/dist/middleware/admin-cors.d.ts.map +1 -0
- package/dist/middleware/admin-cors.js +83 -0
- package/dist/middleware/admin-cors.js.map +1 -0
- package/dist/middleware/admin-mutation-audit.d.ts +9 -0
- package/dist/middleware/admin-mutation-audit.d.ts.map +1 -0
- package/dist/middleware/admin-mutation-audit.js +76 -0
- package/dist/middleware/admin-mutation-audit.js.map +1 -0
- package/dist/middleware/admin-rate-limiter.d.ts +56 -0
- package/dist/middleware/admin-rate-limiter.d.ts.map +1 -0
- package/dist/middleware/admin-rate-limiter.js +97 -0
- package/dist/middleware/admin-rate-limiter.js.map +1 -0
- package/dist/middleware/client-secret-hash.d.ts +34 -0
- package/dist/middleware/client-secret-hash.d.ts.map +1 -0
- package/dist/middleware/client-secret-hash.js +74 -0
- package/dist/middleware/client-secret-hash.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +3 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +93 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/health.d.ts +3 -0
- package/dist/middleware/health.d.ts.map +1 -0
- package/dist/middleware/health.js +35 -0
- package/dist/middleware/health.js.map +1 -0
- package/dist/middleware/metrics.d.ts +40 -0
- package/dist/middleware/metrics.d.ts.map +1 -0
- package/dist/middleware/metrics.js +75 -0
- package/dist/middleware/metrics.js.map +1 -0
- package/dist/middleware/oidc-client-tenant.d.ts +37 -0
- package/dist/middleware/oidc-client-tenant.d.ts.map +1 -0
- package/dist/middleware/oidc-client-tenant.js +156 -0
- package/dist/middleware/oidc-client-tenant.js.map +1 -0
- package/dist/middleware/oidc-cors.d.ts +33 -0
- package/dist/middleware/oidc-cors.d.ts.map +1 -0
- package/dist/middleware/oidc-cors.js +68 -0
- package/dist/middleware/oidc-cors.js.map +1 -0
- package/dist/middleware/oidc-preflight-cors.d.ts +54 -0
- package/dist/middleware/oidc-preflight-cors.d.ts.map +1 -0
- package/dist/middleware/oidc-preflight-cors.js +267 -0
- package/dist/middleware/oidc-preflight-cors.js.map +1 -0
- package/dist/middleware/prompt-login-reset.d.ts +103 -0
- package/dist/middleware/prompt-login-reset.d.ts.map +1 -0
- package/dist/middleware/prompt-login-reset.js +204 -0
- package/dist/middleware/prompt-login-reset.js.map +1 -0
- package/dist/middleware/ready.d.ts +23 -0
- package/dist/middleware/ready.d.ts.map +1 -0
- package/dist/middleware/ready.js +60 -0
- package/dist/middleware/ready.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +27 -0
- package/dist/middleware/request-logger.d.ts.map +1 -0
- package/dist/middleware/request-logger.js +46 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/middleware/require-permission.d.ts +28 -0
- package/dist/middleware/require-permission.d.ts.map +1 -0
- package/dist/middleware/require-permission.js +63 -0
- package/dist/middleware/require-permission.js.map +1 -0
- package/dist/middleware/require-user-organization.d.ts +18 -0
- package/dist/middleware/require-user-organization.d.ts.map +1 -0
- package/dist/middleware/require-user-organization.js +53 -0
- package/dist/middleware/require-user-organization.js.map +1 -0
- package/dist/middleware/root-page.d.ts +62 -0
- package/dist/middleware/root-page.d.ts.map +1 -0
- package/dist/middleware/root-page.js +134 -0
- package/dist/middleware/root-page.js.map +1 -0
- package/dist/middleware/security-headers.d.ts +98 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +159 -0
- package/dist/middleware/security-headers.js.map +1 -0
- package/dist/middleware/tenant-resolver.d.ts +37 -0
- package/dist/middleware/tenant-resolver.d.ts.map +1 -0
- package/dist/middleware/tenant-resolver.js +71 -0
- package/dist/middleware/tenant-resolver.js.map +1 -0
- package/dist/middleware/token-rate-limiter.d.ts +89 -0
- package/dist/middleware/token-rate-limiter.d.ts.map +1 -0
- package/dist/middleware/token-rate-limiter.js +189 -0
- package/dist/middleware/token-rate-limiter.js.map +1 -0
- package/dist/oidc/account-finder.d.ts +52 -0
- package/dist/oidc/account-finder.d.ts.map +1 -0
- package/dist/oidc/account-finder.js +134 -0
- package/dist/oidc/account-finder.js.map +1 -0
- package/dist/oidc/adapter-factory.d.ts +90 -0
- package/dist/oidc/adapter-factory.d.ts.map +1 -0
- package/dist/oidc/adapter-factory.js +165 -0
- package/dist/oidc/adapter-factory.js.map +1 -0
- package/dist/oidc/configuration.d.ts +155 -0
- package/dist/oidc/configuration.d.ts.map +1 -0
- package/dist/oidc/configuration.js +531 -0
- package/dist/oidc/configuration.js.map +1 -0
- package/dist/oidc/postgres-adapter.d.ts +159 -0
- package/dist/oidc/postgres-adapter.d.ts.map +1 -0
- package/dist/oidc/postgres-adapter.js +222 -0
- package/dist/oidc/postgres-adapter.js.map +1 -0
- package/dist/oidc/protocol-security-observer.d.ts +38 -0
- package/dist/oidc/protocol-security-observer.d.ts.map +1 -0
- package/dist/oidc/protocol-security-observer.js +93 -0
- package/dist/oidc/protocol-security-observer.js.map +1 -0
- package/dist/oidc/provider.d.ts +37 -0
- package/dist/oidc/provider.d.ts.map +1 -0
- package/dist/oidc/provider.js +71 -0
- package/dist/oidc/provider.js.map +1 -0
- package/dist/oidc/redis-adapter.d.ts +140 -0
- package/dist/oidc/redis-adapter.d.ts.map +1 -0
- package/dist/oidc/redis-adapter.js +344 -0
- package/dist/oidc/redis-adapter.js.map +1 -0
- package/dist/organizations/cache.d.ts +60 -0
- package/dist/organizations/cache.d.ts.map +1 -0
- package/dist/organizations/cache.js +157 -0
- package/dist/organizations/cache.js.map +1 -0
- package/dist/organizations/errors.d.ts +25 -0
- package/dist/organizations/errors.d.ts.map +1 -0
- package/dist/organizations/errors.js +31 -0
- package/dist/organizations/errors.js.map +1 -0
- package/dist/organizations/index.d.ts +15 -0
- package/dist/organizations/index.d.ts.map +1 -0
- package/dist/organizations/index.js +15 -0
- package/dist/organizations/index.js.map +1 -0
- package/dist/organizations/repository.d.ts +183 -0
- package/dist/organizations/repository.d.ts.map +1 -0
- package/dist/organizations/repository.js +354 -0
- package/dist/organizations/repository.js.map +1 -0
- package/dist/organizations/service.d.ts +168 -0
- package/dist/organizations/service.d.ts.map +1 -0
- package/dist/organizations/service.js +511 -0
- package/dist/organizations/service.js.map +1 -0
- package/dist/organizations/slugs.d.ts +57 -0
- package/dist/organizations/slugs.d.ts.map +1 -0
- package/dist/organizations/slugs.js +130 -0
- package/dist/organizations/slugs.js.map +1 -0
- package/dist/organizations/types.d.ts +146 -0
- package/dist/organizations/types.d.ts.map +1 -0
- package/dist/organizations/types.js +49 -0
- package/dist/organizations/types.js.map +1 -0
- package/dist/rbac/cache.d.ts +110 -0
- package/dist/rbac/cache.d.ts.map +1 -0
- package/dist/rbac/cache.js +271 -0
- package/dist/rbac/cache.js.map +1 -0
- package/dist/rbac/errors.d.ts +34 -0
- package/dist/rbac/errors.d.ts.map +1 -0
- package/dist/rbac/errors.js +43 -0
- package/dist/rbac/errors.js.map +1 -0
- package/dist/rbac/index.d.ts +17 -0
- package/dist/rbac/index.d.ts.map +1 -0
- package/dist/rbac/index.js +21 -0
- package/dist/rbac/index.js.map +1 -0
- package/dist/rbac/mapping-repository.d.ts +121 -0
- package/dist/rbac/mapping-repository.d.ts.map +1 -0
- package/dist/rbac/mapping-repository.js +224 -0
- package/dist/rbac/mapping-repository.js.map +1 -0
- package/dist/rbac/permission-repository.d.ts +100 -0
- package/dist/rbac/permission-repository.d.ts.map +1 -0
- package/dist/rbac/permission-repository.js +190 -0
- package/dist/rbac/permission-repository.js.map +1 -0
- package/dist/rbac/permission-service.d.ts +85 -0
- package/dist/rbac/permission-service.d.ts.map +1 -0
- package/dist/rbac/permission-service.js +179 -0
- package/dist/rbac/permission-service.js.map +1 -0
- package/dist/rbac/role-repository.d.ts +104 -0
- package/dist/rbac/role-repository.d.ts.map +1 -0
- package/dist/rbac/role-repository.js +202 -0
- package/dist/rbac/role-repository.js.map +1 -0
- package/dist/rbac/role-service.d.ts +111 -0
- package/dist/rbac/role-service.d.ts.map +1 -0
- package/dist/rbac/role-service.js +256 -0
- package/dist/rbac/role-service.js.map +1 -0
- package/dist/rbac/slugs.d.ts +116 -0
- package/dist/rbac/slugs.d.ts.map +1 -0
- package/dist/rbac/slugs.js +178 -0
- package/dist/rbac/slugs.js.map +1 -0
- package/dist/rbac/types.d.ts +187 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +75 -0
- package/dist/rbac/types.js.map +1 -0
- package/dist/rbac/user-role-service.d.ts +101 -0
- package/dist/rbac/user-role-service.d.ts.map +1 -0
- package/dist/rbac/user-role-service.js +166 -0
- package/dist/rbac/user-role-service.js.map +1 -0
- package/dist/routes/applications.d.ts +40 -0
- package/dist/routes/applications.d.ts.map +1 -0
- package/dist/routes/applications.js +284 -0
- package/dist/routes/applications.js.map +1 -0
- package/dist/routes/audit.d.ts +31 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +140 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/branding.d.ts +17 -0
- package/dist/routes/branding.d.ts.map +1 -0
- package/dist/routes/branding.js +104 -0
- package/dist/routes/branding.js.map +1 -0
- package/dist/routes/bulk.d.ts +37 -0
- package/dist/routes/bulk.d.ts.map +1 -0
- package/dist/routes/bulk.js +94 -0
- package/dist/routes/bulk.js.map +1 -0
- package/dist/routes/clients.d.ts +44 -0
- package/dist/routes/clients.d.ts.map +1 -0
- package/dist/routes/clients.js +353 -0
- package/dist/routes/clients.js.map +1 -0
- package/dist/routes/config.d.ts +35 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +115 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/custom-claims.d.ts +38 -0
- package/dist/routes/custom-claims.d.ts.map +1 -0
- package/dist/routes/custom-claims.js +208 -0
- package/dist/routes/custom-claims.js.map +1 -0
- package/dist/routes/exports.d.ts +21 -0
- package/dist/routes/exports.d.ts.map +1 -0
- package/dist/routes/exports.js +98 -0
- package/dist/routes/exports.js.map +1 -0
- package/dist/routes/imports.d.ts +18 -0
- package/dist/routes/imports.d.ts.map +1 -0
- package/dist/routes/imports.js +72 -0
- package/dist/routes/imports.js.map +1 -0
- package/dist/routes/interactions.d.ts +111 -0
- package/dist/routes/interactions.d.ts.map +1 -0
- package/dist/routes/interactions.js +1074 -0
- package/dist/routes/interactions.js.map +1 -0
- package/dist/routes/invitation.d.ts +31 -0
- package/dist/routes/invitation.d.ts.map +1 -0
- package/dist/routes/invitation.js +342 -0
- package/dist/routes/invitation.js.map +1 -0
- package/dist/routes/keys.d.ts +28 -0
- package/dist/routes/keys.d.ts.map +1 -0
- package/dist/routes/keys.js +91 -0
- package/dist/routes/keys.js.map +1 -0
- package/dist/routes/magic-link.d.ts +43 -0
- package/dist/routes/magic-link.d.ts.map +1 -0
- package/dist/routes/magic-link.js +264 -0
- package/dist/routes/magic-link.js.map +1 -0
- package/dist/routes/organizations.d.ts +38 -0
- package/dist/routes/organizations.d.ts.map +1 -0
- package/dist/routes/organizations.js +347 -0
- package/dist/routes/organizations.js.map +1 -0
- package/dist/routes/password-reset.d.ts +65 -0
- package/dist/routes/password-reset.d.ts.map +1 -0
- package/dist/routes/password-reset.js +493 -0
- package/dist/routes/password-reset.js.map +1 -0
- package/dist/routes/permissions.d.ts +33 -0
- package/dist/routes/permissions.d.ts.map +1 -0
- package/dist/routes/permissions.js +166 -0
- package/dist/routes/permissions.js.map +1 -0
- package/dist/routes/roles.d.ts +37 -0
- package/dist/routes/roles.d.ts.map +1 -0
- package/dist/routes/roles.js +222 -0
- package/dist/routes/roles.js.map +1 -0
- package/dist/routes/sessions.d.ts +37 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +121 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/stats.d.ts +28 -0
- package/dist/routes/stats.d.ts.map +1 -0
- package/dist/routes/stats.js +65 -0
- package/dist/routes/stats.js.map +1 -0
- package/dist/routes/two-factor-admin.d.ts +54 -0
- package/dist/routes/two-factor-admin.d.ts.map +1 -0
- package/dist/routes/two-factor-admin.js +344 -0
- package/dist/routes/two-factor-admin.js.map +1 -0
- package/dist/routes/two-factor.d.ts +31 -0
- package/dist/routes/two-factor.d.ts.map +1 -0
- package/dist/routes/two-factor.js +595 -0
- package/dist/routes/two-factor.js.map +1 -0
- package/dist/routes/user-roles.d.ts +33 -0
- package/dist/routes/user-roles.d.ts.map +1 -0
- package/dist/routes/user-roles.js +129 -0
- package/dist/routes/user-roles.js.map +1 -0
- package/dist/routes/users.d.ts +61 -0
- package/dist/routes/users.d.ts.map +1 -0
- package/dist/routes/users.js +868 -0
- package/dist/routes/users.js.map +1 -0
- package/dist/security/decision-context.d.ts +52 -0
- package/dist/security/decision-context.d.ts.map +1 -0
- package/dist/security/decision-context.js +150 -0
- package/dist/security/decision-context.js.map +1 -0
- package/dist/security/decision-event.d.ts +128 -0
- package/dist/security/decision-event.d.ts.map +1 -0
- package/dist/security/decision-event.js +225 -0
- package/dist/security/decision-event.js.map +1 -0
- package/dist/security/transport-decision.d.ts +6 -0
- package/dist/security/transport-decision.d.ts.map +1 -0
- package/dist/security/transport-decision.js +37 -0
- package/dist/security/transport-decision.js.map +1 -0
- package/dist/server.d.ts +50 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +491 -0
- package/dist/server.js.map +1 -0
- package/dist/two-factor/cache.d.ts +47 -0
- package/dist/two-factor/cache.d.ts.map +1 -0
- package/dist/two-factor/cache.js +122 -0
- package/dist/two-factor/cache.js.map +1 -0
- package/dist/two-factor/crypto.d.ts +41 -0
- package/dist/two-factor/crypto.d.ts.map +1 -0
- package/dist/two-factor/crypto.js +94 -0
- package/dist/two-factor/crypto.js.map +1 -0
- package/dist/two-factor/errors.d.ts +82 -0
- package/dist/two-factor/errors.d.ts.map +1 -0
- package/dist/two-factor/errors.js +112 -0
- package/dist/two-factor/errors.js.map +1 -0
- package/dist/two-factor/index.d.ts +18 -0
- package/dist/two-factor/index.d.ts.map +1 -0
- package/dist/two-factor/index.js +25 -0
- package/dist/two-factor/index.js.map +1 -0
- package/dist/two-factor/otp.d.ts +43 -0
- package/dist/two-factor/otp.d.ts.map +1 -0
- package/dist/two-factor/otp.js +57 -0
- package/dist/two-factor/otp.js.map +1 -0
- package/dist/two-factor/recovery.d.ts +45 -0
- package/dist/two-factor/recovery.d.ts.map +1 -0
- package/dist/two-factor/recovery.js +104 -0
- package/dist/two-factor/recovery.js.map +1 -0
- package/dist/two-factor/repository.d.ts +156 -0
- package/dist/two-factor/repository.d.ts.map +1 -0
- package/dist/two-factor/repository.js +251 -0
- package/dist/two-factor/repository.js.map +1 -0
- package/dist/two-factor/service.d.ts +232 -0
- package/dist/two-factor/service.d.ts.map +1 -0
- package/dist/two-factor/service.js +599 -0
- package/dist/two-factor/service.js.map +1 -0
- package/dist/two-factor/totp.d.ts +57 -0
- package/dist/two-factor/totp.d.ts.map +1 -0
- package/dist/two-factor/totp.js +96 -0
- package/dist/two-factor/totp.js.map +1 -0
- package/dist/two-factor/types.d.ts +152 -0
- package/dist/two-factor/types.d.ts.map +1 -0
- package/dist/two-factor/types.js +64 -0
- package/dist/two-factor/types.js.map +1 -0
- package/dist/users/cache.d.ts +49 -0
- package/dist/users/cache.d.ts.map +1 -0
- package/dist/users/cache.js +140 -0
- package/dist/users/cache.js.map +1 -0
- package/dist/users/claims.d.ts +67 -0
- package/dist/users/claims.d.ts.map +1 -0
- package/dist/users/claims.js +104 -0
- package/dist/users/claims.js.map +1 -0
- package/dist/users/errors.d.ts +25 -0
- package/dist/users/errors.d.ts.map +1 -0
- package/dist/users/errors.js +31 -0
- package/dist/users/errors.js.map +1 -0
- package/dist/users/gdpr.d.ts +110 -0
- package/dist/users/gdpr.d.ts.map +1 -0
- package/dist/users/gdpr.js +211 -0
- package/dist/users/gdpr.js.map +1 -0
- package/dist/users/index.d.ts +20 -0
- package/dist/users/index.d.ts.map +1 -0
- package/dist/users/index.js +19 -0
- package/dist/users/index.js.map +1 -0
- package/dist/users/password.d.ts +85 -0
- package/dist/users/password.d.ts.map +1 -0
- package/dist/users/password.js +138 -0
- package/dist/users/password.js.map +1 -0
- package/dist/users/repository.d.ts +263 -0
- package/dist/users/repository.d.ts.map +1 -0
- package/dist/users/repository.js +499 -0
- package/dist/users/repository.js.map +1 -0
- package/dist/users/service.d.ts +299 -0
- package/dist/users/service.d.ts.map +1 -0
- package/dist/users/service.js +698 -0
- package/dist/users/service.js.map +1 -0
- package/dist/users/types.d.ts +220 -0
- package/dist/users/types.d.ts.map +1 -0
- package/dist/users/types.js +77 -0
- package/dist/users/types.js.map +1 -0
- package/locales/default/en/common.json +10 -0
- package/locales/default/en/consent.json +13 -0
- package/locales/default/en/emails.json +17 -0
- package/locales/default/en/errors.json +21 -0
- package/locales/default/en/forgot-password.json +11 -0
- package/locales/default/en/invitation.json +18 -0
- package/locales/default/en/login.json +17 -0
- package/locales/default/en/logout.json +9 -0
- package/locales/default/en/magic-link.json +12 -0
- package/locales/default/en/reset-password.json +16 -0
- package/locales/default/en/two-factor.json +29 -0
- package/migrations/.gitkeep +0 -0
- package/migrations/001_extensions.sql +22 -0
- package/migrations/002_organizations.sql +43 -0
- package/migrations/003_applications.sql +50 -0
- package/migrations/004_clients.sql +74 -0
- package/migrations/005_users.sql +119 -0
- package/migrations/006_roles_permissions.sql +80 -0
- package/migrations/007_custom_claims.sql +55 -0
- package/migrations/008_config.sql +51 -0
- package/migrations/009_audit_log.sql +31 -0
- package/migrations/010_oidc_adapter.sql +34 -0
- package/migrations/011_seed.sql +53 -0
- package/migrations/012_two_factor.sql +88 -0
- package/migrations/013_client_secret_sha256.sql +22 -0
- package/migrations/014_login_methods.sql +34 -0
- package/migrations/015_signing_key_encryption.sql +20 -0
- package/migrations/016_failed_login_count.sql +27 -0
- package/migrations/017_audit_retention.sql +11 -0
- package/migrations/018_admin_api_enhancements.sql +78 -0
- package/migrations/019_invitation_details.sql +27 -0
- package/migrations/020_two_factor_admin_permission.sql +108 -0
- package/migrations/021_auth_recovery_jobs.sql +74 -0
- package/migrations/022_recovery_artifact_ownership.sql +25 -0
- package/migrations/023_magic_link_authority.sql +39 -0
- package/package.json +106 -0
- package/templates/default/emails/invitation.hbs +39 -0
- package/templates/default/emails/invitation.txt.hbs +18 -0
- package/templates/default/emails/magic-link.hbs +30 -0
- package/templates/default/emails/magic-link.txt.hbs +11 -0
- package/templates/default/emails/otp-code.hbs +31 -0
- package/templates/default/emails/otp-code.txt.hbs +15 -0
- package/templates/default/emails/password-changed.hbs +24 -0
- package/templates/default/emails/password-changed.txt.hbs +7 -0
- package/templates/default/emails/password-reset.hbs +30 -0
- package/templates/default/emails/password-reset.txt.hbs +11 -0
- package/templates/default/emails/welcome.hbs +25 -0
- package/templates/default/emails/welcome.txt.hbs +9 -0
- package/templates/default/layouts/main.hbs +49 -0
- package/templates/default/pages/accept-invite.hbs +25 -0
- package/templates/default/pages/consent.hbs +27 -0
- package/templates/default/pages/error.hbs +15 -0
- package/templates/default/pages/forgot-password.hbs +21 -0
- package/templates/default/pages/invite-expired.hbs +6 -0
- package/templates/default/pages/invite-success.hbs +9 -0
- package/templates/default/pages/login.hbs +108 -0
- package/templates/default/pages/logout-success.hbs +5 -0
- package/templates/default/pages/logout.hbs +24 -0
- package/templates/default/pages/magic-link-sent.hbs +10 -0
- package/templates/default/pages/magic-link-success.hbs +17 -0
- package/templates/default/pages/reset-password.hbs +20 -0
- package/templates/default/pages/reset-success.hbs +9 -0
- package/templates/default/pages/two-factor-setup.hbs +65 -0
- package/templates/default/pages/two-factor-verify.hbs +77 -0
- package/templates/default/partials/flash-messages.hbs +10 -0
- package/templates/default/partials/footer.hbs +4 -0
- package/templates/default/partials/header.hbs +8 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User management API routes.
|
|
3
|
+
*
|
|
4
|
+
* All routes are under `/api/admin/organizations/:orgId/users` and
|
|
5
|
+
* require admin authorization with granular permissions. User routes
|
|
6
|
+
* are nested under an organization context because users are always
|
|
7
|
+
* scoped to an org.
|
|
8
|
+
*
|
|
9
|
+
* Route structure:
|
|
10
|
+
* POST / — Create a user in the organization
|
|
11
|
+
* GET / — List users (paginated, searchable)
|
|
12
|
+
* GET /:userId — Get user by ID
|
|
13
|
+
* PUT /:userId — Update user profile
|
|
14
|
+
* POST /:userId/deactivate — Deactivate (active → inactive)
|
|
15
|
+
* POST /:userId/reactivate — Reactivate (inactive → active)
|
|
16
|
+
* POST /:userId/suspend — Suspend (active → suspended)
|
|
17
|
+
* POST /:userId/unsuspend — Unsuspend (suspended → active)
|
|
18
|
+
* POST /:userId/lock — Lock (active → locked)
|
|
19
|
+
* POST /:userId/unlock — Unlock (locked → active)
|
|
20
|
+
* POST /:userId/password — Set/change password
|
|
21
|
+
* DELETE /:userId/password — Clear password (passwordless)
|
|
22
|
+
* POST /:userId/verify-email — Mark email as verified
|
|
23
|
+
* GET /:userId/export — GDPR data export (Article 20)
|
|
24
|
+
* POST /:userId/purge — GDPR data purge (Article 17)
|
|
25
|
+
*
|
|
26
|
+
* Error mapping:
|
|
27
|
+
* UserNotFoundError → 404
|
|
28
|
+
* UserValidationError → 400
|
|
29
|
+
* SuperAdminProtectionError → 403
|
|
30
|
+
* ZodError → 400 with validation details
|
|
31
|
+
*/
|
|
32
|
+
import Router from '@koa/router';
|
|
33
|
+
import { z } from 'zod';
|
|
34
|
+
import { renderInvitationEmail, sendInvitationEmail } from '../auth/email-service.js';
|
|
35
|
+
import { insertInvitationToken, invalidateUserTokens } from '../auth/token-repository.js';
|
|
36
|
+
import { generateToken } from '../auth/tokens.js';
|
|
37
|
+
import { config } from '../config/index.js';
|
|
38
|
+
import { ADMIN_PERMISSIONS } from '../lib/admin-permissions.js';
|
|
39
|
+
import { writeAuditLog } from '../lib/audit-log.js';
|
|
40
|
+
import { afterDatabaseCommit, getPool } from '../lib/database.js';
|
|
41
|
+
import { getEntityHistory } from '../lib/entity-history.js';
|
|
42
|
+
import { checkIfMatch, setETagHeader } from '../lib/etag.js';
|
|
43
|
+
import { guardSuperAdmin, SuperAdminProtectionError } from '../lib/super-admin-protection.js';
|
|
44
|
+
import { requireAdminAuth } from '../middleware/admin-auth.js';
|
|
45
|
+
import { requirePermission } from '../middleware/require-permission.js';
|
|
46
|
+
import { requireUserOrganization } from '../middleware/require-user-organization.js';
|
|
47
|
+
import { getOrganizationById } from '../organizations/service.js';
|
|
48
|
+
import { UserNotFoundError, UserValidationError } from '../users/errors.js';
|
|
49
|
+
import { exportUserData, purgeUserData } from '../users/gdpr.js';
|
|
50
|
+
import * as userService from '../users/service.js';
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Validation schemas
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/** Schema for creating a new user */
|
|
55
|
+
const createUserSchema = z.object({
|
|
56
|
+
email: z.string().email().max(255),
|
|
57
|
+
password: z.string().min(8).max(128).optional(),
|
|
58
|
+
givenName: z.string().max(255).optional(),
|
|
59
|
+
familyName: z.string().max(255).optional(),
|
|
60
|
+
middleName: z.string().max(255).optional(),
|
|
61
|
+
nickname: z.string().max(255).optional(),
|
|
62
|
+
preferredUsername: z.string().max(255).optional(),
|
|
63
|
+
profileUrl: z.string().url().optional(),
|
|
64
|
+
pictureUrl: z.string().url().optional(),
|
|
65
|
+
websiteUrl: z.string().url().optional(),
|
|
66
|
+
gender: z.string().max(50).optional(),
|
|
67
|
+
birthdate: z
|
|
68
|
+
.string()
|
|
69
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
70
|
+
.optional(),
|
|
71
|
+
zoneinfo: z.string().max(50).optional(),
|
|
72
|
+
locale: z.string().max(10).optional(),
|
|
73
|
+
phoneNumber: z.string().max(50).optional(),
|
|
74
|
+
phoneNumberVerified: z.boolean().optional(),
|
|
75
|
+
address: z
|
|
76
|
+
.object({
|
|
77
|
+
street: z.string().nullable().optional(),
|
|
78
|
+
locality: z.string().max(255).nullable().optional(),
|
|
79
|
+
region: z.string().max(255).nullable().optional(),
|
|
80
|
+
postalCode: z.string().max(20).nullable().optional(),
|
|
81
|
+
country: z.string().length(2).nullable().optional(),
|
|
82
|
+
})
|
|
83
|
+
.optional(),
|
|
84
|
+
});
|
|
85
|
+
/** Schema for updating a user (all fields optional, nullable for clearing) */
|
|
86
|
+
const updateUserSchema = z.object({
|
|
87
|
+
givenName: z.string().max(255).nullable().optional(),
|
|
88
|
+
familyName: z.string().max(255).nullable().optional(),
|
|
89
|
+
middleName: z.string().max(255).nullable().optional(),
|
|
90
|
+
nickname: z.string().max(255).nullable().optional(),
|
|
91
|
+
preferredUsername: z.string().max(255).nullable().optional(),
|
|
92
|
+
profileUrl: z.string().url().nullable().optional(),
|
|
93
|
+
pictureUrl: z.string().url().nullable().optional(),
|
|
94
|
+
websiteUrl: z.string().url().nullable().optional(),
|
|
95
|
+
gender: z.string().max(50).nullable().optional(),
|
|
96
|
+
birthdate: z
|
|
97
|
+
.string()
|
|
98
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
99
|
+
.nullable()
|
|
100
|
+
.optional(),
|
|
101
|
+
zoneinfo: z.string().max(50).nullable().optional(),
|
|
102
|
+
locale: z.string().max(10).nullable().optional(),
|
|
103
|
+
phoneNumber: z.string().max(50).nullable().optional(),
|
|
104
|
+
phoneNumberVerified: z.boolean().optional(),
|
|
105
|
+
address: z
|
|
106
|
+
.object({
|
|
107
|
+
street: z.string().nullable().optional(),
|
|
108
|
+
locality: z.string().max(255).nullable().optional(),
|
|
109
|
+
region: z.string().max(255).nullable().optional(),
|
|
110
|
+
postalCode: z.string().max(20).nullable().optional(),
|
|
111
|
+
country: z.string().length(2).nullable().optional(),
|
|
112
|
+
})
|
|
113
|
+
.nullable()
|
|
114
|
+
.optional(),
|
|
115
|
+
});
|
|
116
|
+
/** Schema for listing users with pagination */
|
|
117
|
+
const listUsersSchema = z.object({
|
|
118
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
119
|
+
pageSize: z.coerce.number().int().min(1).max(100).default(20),
|
|
120
|
+
status: z.enum(['active', 'inactive', 'suspended', 'locked']).optional(),
|
|
121
|
+
search: z.string().max(255).optional(),
|
|
122
|
+
sortBy: z
|
|
123
|
+
.enum(['email', 'given_name', 'family_name', 'created_at', 'last_login_at'])
|
|
124
|
+
.default('created_at'),
|
|
125
|
+
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
|
126
|
+
});
|
|
127
|
+
/** Schema for cursor-based listing (keyset pagination) */
|
|
128
|
+
const listUsersCursorSchema = z.object({
|
|
129
|
+
cursor: z.string().optional(),
|
|
130
|
+
limit: z.coerce.number().int().min(1).max(100).default(25),
|
|
131
|
+
status: z.enum(['active', 'inactive', 'suspended', 'locked']).optional(),
|
|
132
|
+
search: z.string().max(255).optional(),
|
|
133
|
+
sortBy: z
|
|
134
|
+
.enum(['email', 'given_name', 'family_name', 'created_at', 'last_login_at'])
|
|
135
|
+
.default('created_at'),
|
|
136
|
+
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
|
137
|
+
});
|
|
138
|
+
/** Schema for setting a password */
|
|
139
|
+
const setPasswordSchema = z.object({
|
|
140
|
+
password: z.string().min(8).max(128),
|
|
141
|
+
});
|
|
142
|
+
/** Schema for locking a user (reason required) */
|
|
143
|
+
const lockUserSchema = z.object({
|
|
144
|
+
reason: z.string().min(1).max(500),
|
|
145
|
+
});
|
|
146
|
+
/** Schema for suspending a user (reason optional) */
|
|
147
|
+
const suspendUserSchema = z.object({
|
|
148
|
+
reason: z.string().max(500).optional(),
|
|
149
|
+
});
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Error handler helper
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
/**
|
|
154
|
+
* Handle domain errors and map them to HTTP responses.
|
|
155
|
+
* Unknown errors are re-thrown for the global error handler.
|
|
156
|
+
*/
|
|
157
|
+
function handleError(ctx, err) {
|
|
158
|
+
if (err instanceof SuperAdminProtectionError) {
|
|
159
|
+
ctx.status = 403;
|
|
160
|
+
ctx.body = { error: 'Forbidden', message: 'The requested operation is not permitted' };
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
if (err instanceof UserNotFoundError) {
|
|
164
|
+
ctx.throw(404, 'User not found');
|
|
165
|
+
}
|
|
166
|
+
if (err instanceof UserValidationError) {
|
|
167
|
+
ctx.throw(400, 'User request is invalid');
|
|
168
|
+
}
|
|
169
|
+
if (err instanceof z.ZodError) {
|
|
170
|
+
ctx.status = 400;
|
|
171
|
+
ctx.body = { error: 'User request is invalid' };
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Router factory
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
/**
|
|
180
|
+
* Create the user management router.
|
|
181
|
+
*
|
|
182
|
+
* All routes require admin authorization with granular permissions.
|
|
183
|
+
* Users are always scoped to an organization via the :orgId URL parameter.
|
|
184
|
+
* Destructive operations on the super-admin user are blocked by
|
|
185
|
+
* guardSuperAdmin() checks.
|
|
186
|
+
*
|
|
187
|
+
* Prefix: /api/admin/organizations/:orgId/users
|
|
188
|
+
*
|
|
189
|
+
* @returns Configured Koa Router
|
|
190
|
+
*/
|
|
191
|
+
export function createUserRouter() {
|
|
192
|
+
const router = new Router({ prefix: '/api/admin/organizations/:orgId/users' });
|
|
193
|
+
// All routes require admin authentication
|
|
194
|
+
router.use(requireAdminAuth());
|
|
195
|
+
// -------------------------------------------------------------------------
|
|
196
|
+
// POST / — Create user
|
|
197
|
+
// -------------------------------------------------------------------------
|
|
198
|
+
router.post('/', requirePermission(ADMIN_PERMISSIONS.USER_CREATE), async (ctx) => {
|
|
199
|
+
try {
|
|
200
|
+
const body = createUserSchema.parse(ctx.request.body);
|
|
201
|
+
const user = await userService.createUser({
|
|
202
|
+
organizationId: ctx.params.orgId,
|
|
203
|
+
...body,
|
|
204
|
+
});
|
|
205
|
+
ctx.status = 201;
|
|
206
|
+
ctx.body = { data: user };
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
handleError(ctx, err);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
// -------------------------------------------------------------------------
|
|
213
|
+
// GET / — List users (paginated)
|
|
214
|
+
// -------------------------------------------------------------------------
|
|
215
|
+
router.get('/', requirePermission(ADMIN_PERMISSIONS.USER_READ), async (ctx) => {
|
|
216
|
+
try {
|
|
217
|
+
// Cursor-based pagination when `cursor` or `limit` param is present
|
|
218
|
+
if (ctx.query.cursor !== undefined || ctx.query.limit !== undefined) {
|
|
219
|
+
const query = listUsersCursorSchema.parse(ctx.query);
|
|
220
|
+
const result = await userService.listUsersCursor({
|
|
221
|
+
organizationId: ctx.params.orgId,
|
|
222
|
+
...query,
|
|
223
|
+
});
|
|
224
|
+
ctx.body = result;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Default: offset-based pagination (backward compatible)
|
|
228
|
+
const query = listUsersSchema.parse(ctx.query);
|
|
229
|
+
const result = await userService.listUsersByOrganization({
|
|
230
|
+
organizationId: ctx.params.orgId,
|
|
231
|
+
...query,
|
|
232
|
+
});
|
|
233
|
+
ctx.body = result;
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
handleError(ctx, err);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// -------------------------------------------------------------------------
|
|
240
|
+
// GET /:userId — Get user by ID
|
|
241
|
+
// -------------------------------------------------------------------------
|
|
242
|
+
router.get('/:userId', requirePermission(ADMIN_PERMISSIONS.USER_READ), requireUserOrganization(), async (ctx) => {
|
|
243
|
+
const user = await userService.getUserById(ctx.params.userId);
|
|
244
|
+
if (!user) {
|
|
245
|
+
ctx.throw(404, 'User not found');
|
|
246
|
+
return; // unreachable — keeps TS narrowing happy
|
|
247
|
+
}
|
|
248
|
+
setETagHeader(ctx, 'user', user.id, user.updatedAt);
|
|
249
|
+
ctx.body = { data: user };
|
|
250
|
+
});
|
|
251
|
+
// -------------------------------------------------------------------------
|
|
252
|
+
// PUT /:userId — Update user profile
|
|
253
|
+
// -------------------------------------------------------------------------
|
|
254
|
+
router.put('/:userId', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), requireUserOrganization(), async (ctx) => {
|
|
255
|
+
try {
|
|
256
|
+
const body = updateUserSchema.parse(ctx.request.body);
|
|
257
|
+
// Check If-Match for optimistic concurrency (optional — backward compatible)
|
|
258
|
+
const current = await userService.getUserById(ctx.params.userId);
|
|
259
|
+
if (current && !checkIfMatch(ctx, 'user', current.id, current.updatedAt, current))
|
|
260
|
+
return;
|
|
261
|
+
// Convert null address to undefined — Zod allows null for clearing,
|
|
262
|
+
// but UpdateUserInput uses undefined to mean "no change"
|
|
263
|
+
const input = { ...body, address: body.address ?? undefined };
|
|
264
|
+
const user = await userService.updateUser(ctx.params.userId, input);
|
|
265
|
+
setETagHeader(ctx, 'user', user.id, user.updatedAt);
|
|
266
|
+
ctx.body = { data: user };
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
handleError(ctx, err);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
// -------------------------------------------------------------------------
|
|
273
|
+
// POST /:userId/deactivate — Deactivate user
|
|
274
|
+
// Protected: super-admin user cannot be deactivated
|
|
275
|
+
// -------------------------------------------------------------------------
|
|
276
|
+
router.post('/:userId/deactivate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
|
|
277
|
+
try {
|
|
278
|
+
await guardSuperAdmin(ctx.params.userId, 'deactivate');
|
|
279
|
+
await userService.deactivateUser(ctx.params.userId);
|
|
280
|
+
ctx.status = 204;
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
handleError(ctx, err);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
// -------------------------------------------------------------------------
|
|
287
|
+
// POST /:userId/reactivate — Reactivate user
|
|
288
|
+
// -------------------------------------------------------------------------
|
|
289
|
+
router.post('/:userId/reactivate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
|
|
290
|
+
try {
|
|
291
|
+
await userService.reactivateUser(ctx.params.userId);
|
|
292
|
+
ctx.status = 204;
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
handleError(ctx, err);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
// -------------------------------------------------------------------------
|
|
299
|
+
// POST /:userId/suspend — Suspend user
|
|
300
|
+
// Protected: super-admin user cannot be suspended
|
|
301
|
+
// -------------------------------------------------------------------------
|
|
302
|
+
router.post('/:userId/suspend', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
|
|
303
|
+
try {
|
|
304
|
+
await guardSuperAdmin(ctx.params.userId, 'suspend');
|
|
305
|
+
const body = suspendUserSchema.parse(ctx.request.body ?? {});
|
|
306
|
+
await userService.suspendUser(ctx.params.userId, body.reason);
|
|
307
|
+
ctx.status = 204;
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
handleError(ctx, err);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
// -------------------------------------------------------------------------
|
|
314
|
+
// POST /:userId/unsuspend — Unsuspend user
|
|
315
|
+
// -------------------------------------------------------------------------
|
|
316
|
+
router.post('/:userId/unsuspend', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
|
|
317
|
+
try {
|
|
318
|
+
await userService.unsuspendUser(ctx.params.userId);
|
|
319
|
+
ctx.status = 204;
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
handleError(ctx, err);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
// -------------------------------------------------------------------------
|
|
326
|
+
// POST /:userId/lock — Lock user
|
|
327
|
+
// Protected: super-admin user cannot be locked
|
|
328
|
+
// -------------------------------------------------------------------------
|
|
329
|
+
router.post('/:userId/lock', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
|
|
330
|
+
try {
|
|
331
|
+
await guardSuperAdmin(ctx.params.userId, 'lock');
|
|
332
|
+
const body = lockUserSchema.parse(ctx.request.body);
|
|
333
|
+
await userService.lockUser(ctx.params.userId, body.reason);
|
|
334
|
+
ctx.status = 204;
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
handleError(ctx, err);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
// -------------------------------------------------------------------------
|
|
341
|
+
// POST /:userId/unlock — Unlock user
|
|
342
|
+
// -------------------------------------------------------------------------
|
|
343
|
+
router.post('/:userId/unlock', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), requireUserOrganization(), async (ctx) => {
|
|
344
|
+
try {
|
|
345
|
+
await userService.unlockUser(ctx.params.userId);
|
|
346
|
+
ctx.status = 204;
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
handleError(ctx, err);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
// -------------------------------------------------------------------------
|
|
353
|
+
// POST /:userId/password — Set password
|
|
354
|
+
// -------------------------------------------------------------------------
|
|
355
|
+
router.post('/:userId/password', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), requireUserOrganization(), async (ctx) => {
|
|
356
|
+
try {
|
|
357
|
+
const body = setPasswordSchema.parse(ctx.request.body);
|
|
358
|
+
await userService.setUserPassword(ctx.params.userId, body.password);
|
|
359
|
+
ctx.status = 204;
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
handleError(ctx, err);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
// -------------------------------------------------------------------------
|
|
366
|
+
// DELETE /:userId/password — Clear password (convert to passwordless)
|
|
367
|
+
// -------------------------------------------------------------------------
|
|
368
|
+
router.delete('/:userId/password', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), requireUserOrganization(), async (ctx) => {
|
|
369
|
+
try {
|
|
370
|
+
await userService.clearUserPassword(ctx.params.userId);
|
|
371
|
+
ctx.status = 204;
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
handleError(ctx, err);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
// -------------------------------------------------------------------------
|
|
378
|
+
// POST /:userId/verify-email — Mark email as verified
|
|
379
|
+
// -------------------------------------------------------------------------
|
|
380
|
+
router.post('/:userId/verify-email', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), requireUserOrganization(), async (ctx) => {
|
|
381
|
+
try {
|
|
382
|
+
await userService.markEmailVerified(ctx.params.userId);
|
|
383
|
+
ctx.status = 204;
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
handleError(ctx, err);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
// -------------------------------------------------------------------------
|
|
390
|
+
// GET /:userId/export — GDPR data export (Article 20)
|
|
391
|
+
// -------------------------------------------------------------------------
|
|
392
|
+
router.get('/:userId/export', requirePermission(ADMIN_PERMISSIONS.USER_READ), requireUserOrganization(), async (ctx) => {
|
|
393
|
+
const user = await userService.getUserById(ctx.params.userId);
|
|
394
|
+
if (!user) {
|
|
395
|
+
return ctx.throw(404, 'User not found');
|
|
396
|
+
}
|
|
397
|
+
const exportData = await exportUserData(user);
|
|
398
|
+
ctx.body = { data: exportData };
|
|
399
|
+
});
|
|
400
|
+
// -------------------------------------------------------------------------
|
|
401
|
+
// POST /:userId/purge — GDPR data purge (Article 17)
|
|
402
|
+
//
|
|
403
|
+
// Requires X-Confirm-Purge: true header for safety.
|
|
404
|
+
// Irreversibly anonymizes user data and deletes related records.
|
|
405
|
+
// Protected: super-admin user cannot be purged
|
|
406
|
+
// -------------------------------------------------------------------------
|
|
407
|
+
router.post('/:userId/purge', requirePermission(ADMIN_PERMISSIONS.USER_ARCHIVE), requireUserOrganization(), async (ctx) => {
|
|
408
|
+
// Require explicit confirmation via header OR request body
|
|
409
|
+
const confirmHeader = ctx.get('X-Confirm-Purge');
|
|
410
|
+
const confirmBody = ctx.request.body?.confirmPurge;
|
|
411
|
+
if (confirmHeader !== 'true' && confirmBody !== true) {
|
|
412
|
+
ctx.status = 400;
|
|
413
|
+
ctx.body = {
|
|
414
|
+
error: 'Purge requires confirmation',
|
|
415
|
+
message: 'Set X-Confirm-Purge: true header or send { "confirmPurge": true } in body',
|
|
416
|
+
};
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const user = await userService.getUserById(ctx.params.userId);
|
|
420
|
+
if (!user) {
|
|
421
|
+
return ctx.throw(404, 'User not found');
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
// Guard: super-admin user cannot be purged
|
|
425
|
+
await guardSuperAdmin(ctx.params.userId, 'delete');
|
|
426
|
+
// Use the admin user's ID as the actor for the audit trail
|
|
427
|
+
const actorId = ctx.state.adminUser?.id ?? 'system';
|
|
428
|
+
const result = await purgeUserData(user, actorId);
|
|
429
|
+
ctx.body = { data: result };
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
if (err instanceof SuperAdminProtectionError) {
|
|
433
|
+
ctx.status = 403;
|
|
434
|
+
ctx.body = { error: 'The requested operation is not permitted' };
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (err instanceof Error && err.message.includes('super-admin')) {
|
|
438
|
+
ctx.status = 403;
|
|
439
|
+
ctx.body = { error: 'The requested operation is not permitted' };
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// -------------------------------------------------------------------------
|
|
446
|
+
// GET /:userId/history — User change history
|
|
447
|
+
// -------------------------------------------------------------------------
|
|
448
|
+
router.get('/:userId/history', requirePermission(ADMIN_PERMISSIONS.USER_READ), requireUserOrganization(), async (ctx) => {
|
|
449
|
+
const { limit, after, event_type } = ctx.query;
|
|
450
|
+
const result = await getEntityHistory('user', ctx.params.userId, {
|
|
451
|
+
limit: limit ? parseInt(limit, 10) : undefined,
|
|
452
|
+
after: after || undefined,
|
|
453
|
+
eventTypePrefix: event_type || undefined,
|
|
454
|
+
});
|
|
455
|
+
ctx.body = result;
|
|
456
|
+
});
|
|
457
|
+
// -------------------------------------------------------------------------
|
|
458
|
+
// POST /invite — Send invitation to a new or existing user
|
|
459
|
+
//
|
|
460
|
+
// Enhanced invitation with optional personal message, role/claim
|
|
461
|
+
// pre-assignment, and inviter tracking. Creates the user if they
|
|
462
|
+
// don't exist, generates an invitation token with pre-assignment
|
|
463
|
+
// details, and sends the invitation email.
|
|
464
|
+
// -------------------------------------------------------------------------
|
|
465
|
+
router.post('/invite', requirePermission(ADMIN_PERMISSIONS.USER_INVITE), async (ctx) => {
|
|
466
|
+
try {
|
|
467
|
+
const body = inviteUserSchema.parse(ctx.request.body);
|
|
468
|
+
const orgId = ctx.params.orgId;
|
|
469
|
+
const adminUser = ctx.state.adminUser;
|
|
470
|
+
// Resolve the organization for branding and slug
|
|
471
|
+
const org = await getOrganizationById(orgId);
|
|
472
|
+
if (!org) {
|
|
473
|
+
ctx.throw(404, 'Organization not found');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
// Validate referenced applicationIds, roleIds, claimDefinitionIds exist
|
|
477
|
+
if (body.roles?.length || body.claims?.length) {
|
|
478
|
+
await validatePreAssignments(orgId, body.roles, body.claims);
|
|
479
|
+
}
|
|
480
|
+
// Find or create the user
|
|
481
|
+
let user = await userService.getUserByEmail(orgId, body.email);
|
|
482
|
+
let created = false;
|
|
483
|
+
if (!user) {
|
|
484
|
+
user = await userService.createUser({
|
|
485
|
+
organizationId: orgId,
|
|
486
|
+
email: body.email,
|
|
487
|
+
givenName: body.givenName,
|
|
488
|
+
familyName: body.familyName,
|
|
489
|
+
});
|
|
490
|
+
created = true;
|
|
491
|
+
}
|
|
492
|
+
// Invalidate any previous pending invitation tokens for this user
|
|
493
|
+
await invalidateUserTokens('invitation_tokens', user.id);
|
|
494
|
+
// Generate a new invitation token
|
|
495
|
+
const { plaintext, hash } = generateToken();
|
|
496
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
|
497
|
+
// Build inviter display name
|
|
498
|
+
const inviterName = adminUser.givenName
|
|
499
|
+
? adminUser.familyName
|
|
500
|
+
? `${adminUser.givenName} ${adminUser.familyName}`
|
|
501
|
+
: adminUser.givenName
|
|
502
|
+
: (adminUser.email ?? 'Admin');
|
|
503
|
+
// Store pre-assignment details in the token
|
|
504
|
+
const details = {};
|
|
505
|
+
if (body.personalMessage)
|
|
506
|
+
details.personalMessage = body.personalMessage;
|
|
507
|
+
if (body.roles?.length)
|
|
508
|
+
details.roles = body.roles;
|
|
509
|
+
if (body.claims?.length)
|
|
510
|
+
details.claims = body.claims;
|
|
511
|
+
details.inviterName = inviterName;
|
|
512
|
+
await insertInvitationToken(user.id, hash, expiresAt, Object.keys(details).length > 0 ? details : null, adminUser.id);
|
|
513
|
+
// Build the invitation URL. Prefix the trusted, configured issuerBaseUrl
|
|
514
|
+
// (never a request header) so the email link is absolute and includes
|
|
515
|
+
// the server origin — matching the password-reset / magic-link pattern.
|
|
516
|
+
const inviteUrl = `${config.issuerBaseUrl}/${org.slug}/auth/accept-invite/${plaintext}`;
|
|
517
|
+
// Send the invitation email
|
|
518
|
+
const emailOptions = {};
|
|
519
|
+
if (body.personalMessage)
|
|
520
|
+
emailOptions.personalMessage = body.personalMessage;
|
|
521
|
+
emailOptions.inviterName = inviterName;
|
|
522
|
+
await afterDatabaseCommit(() => sendInvitationEmail({
|
|
523
|
+
id: user.id,
|
|
524
|
+
email: user.email,
|
|
525
|
+
givenName: user.givenName,
|
|
526
|
+
familyName: user.familyName,
|
|
527
|
+
}, {
|
|
528
|
+
id: org.id,
|
|
529
|
+
slug: org.slug,
|
|
530
|
+
brandingLogoUrl: org.brandingLogoUrl,
|
|
531
|
+
brandingPrimaryColor: org.brandingPrimaryColor,
|
|
532
|
+
brandingCompanyName: org.brandingCompanyName,
|
|
533
|
+
}, inviteUrl, body.locale ?? org.defaultLocale ?? 'en', emailOptions));
|
|
534
|
+
// Audit log the invitation
|
|
535
|
+
writeAuditLog({
|
|
536
|
+
organizationId: orgId,
|
|
537
|
+
userId: user.id,
|
|
538
|
+
actorId: adminUser.id,
|
|
539
|
+
eventType: 'user.invited',
|
|
540
|
+
eventCategory: 'admin',
|
|
541
|
+
description: 'User invitation created',
|
|
542
|
+
metadata: {
|
|
543
|
+
hasPersonalMessage: !!body.personalMessage,
|
|
544
|
+
preAssignedRoles: body.roles?.length ?? 0,
|
|
545
|
+
preAssignedClaims: body.claims?.length ?? 0,
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
ctx.status = created ? 201 : 200;
|
|
549
|
+
ctx.body = {
|
|
550
|
+
data: {
|
|
551
|
+
userId: user.id,
|
|
552
|
+
email: user.email,
|
|
553
|
+
created,
|
|
554
|
+
invitationSent: true,
|
|
555
|
+
expiresAt: expiresAt.toISOString(),
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
handleError(ctx, err);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
// -------------------------------------------------------------------------
|
|
564
|
+
// POST /invite/preview — Preview invitation email without sending
|
|
565
|
+
//
|
|
566
|
+
// Renders the invitation email with the provided parameters and returns
|
|
567
|
+
// the HTML, plain text, and subject line for admin review.
|
|
568
|
+
// -------------------------------------------------------------------------
|
|
569
|
+
router.post('/invite/preview', requirePermission(ADMIN_PERMISSIONS.USER_INVITE), async (ctx) => {
|
|
570
|
+
try {
|
|
571
|
+
const body = invitePreviewSchema.parse(ctx.request.body);
|
|
572
|
+
const orgId = ctx.params.orgId;
|
|
573
|
+
const adminUser = ctx.state.adminUser;
|
|
574
|
+
// Resolve the organization for branding
|
|
575
|
+
const org = await getOrganizationById(orgId);
|
|
576
|
+
if (!org) {
|
|
577
|
+
ctx.throw(404, 'Organization not found');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
// Build inviter display name
|
|
581
|
+
const inviterName = adminUser.givenName
|
|
582
|
+
? adminUser.familyName
|
|
583
|
+
? `${adminUser.givenName} ${adminUser.familyName}`
|
|
584
|
+
: adminUser.givenName
|
|
585
|
+
: (adminUser.email ?? 'Admin');
|
|
586
|
+
// Build a mock user for the preview
|
|
587
|
+
const previewUser = {
|
|
588
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
589
|
+
email: body.email,
|
|
590
|
+
givenName: body.givenName ?? null,
|
|
591
|
+
familyName: body.familyName ?? null,
|
|
592
|
+
};
|
|
593
|
+
// Render the invitation email (without sending)
|
|
594
|
+
const emailOptions = {};
|
|
595
|
+
if (body.personalMessage)
|
|
596
|
+
emailOptions.personalMessage = body.personalMessage;
|
|
597
|
+
emailOptions.inviterName = inviterName;
|
|
598
|
+
const result = await renderInvitationEmail(previewUser, {
|
|
599
|
+
id: org.id,
|
|
600
|
+
slug: org.slug,
|
|
601
|
+
brandingLogoUrl: org.brandingLogoUrl,
|
|
602
|
+
brandingPrimaryColor: org.brandingPrimaryColor,
|
|
603
|
+
brandingCompanyName: org.brandingCompanyName,
|
|
604
|
+
},
|
|
605
|
+
// Absolute preview URL — same trusted issuerBaseUrl prefix as the real invite.
|
|
606
|
+
`${config.issuerBaseUrl}/${org.slug}/auth/accept-invite/PREVIEW_TOKEN`, body.locale ?? org.defaultLocale ?? 'en', emailOptions);
|
|
607
|
+
ctx.body = { data: result };
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
handleError(ctx, err);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
return router;
|
|
614
|
+
}
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
// Invitation schemas
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
/** Schema for the enhanced invitation request */
|
|
619
|
+
const inviteUserSchema = z.object({
|
|
620
|
+
email: z.string().email(),
|
|
621
|
+
givenName: z.string().min(1).max(255).optional(),
|
|
622
|
+
familyName: z.string().min(1).max(255).optional(),
|
|
623
|
+
personalMessage: z.string().max(500).optional(),
|
|
624
|
+
roles: z
|
|
625
|
+
.array(z.object({
|
|
626
|
+
applicationId: z.string().uuid(),
|
|
627
|
+
roleId: z.string().uuid(),
|
|
628
|
+
}))
|
|
629
|
+
.optional(),
|
|
630
|
+
claims: z
|
|
631
|
+
.array(z.object({
|
|
632
|
+
applicationId: z.string().uuid(),
|
|
633
|
+
claimDefinitionId: z.string().uuid(),
|
|
634
|
+
value: z.unknown(),
|
|
635
|
+
}))
|
|
636
|
+
.optional(),
|
|
637
|
+
locale: z.string().max(10).optional(),
|
|
638
|
+
});
|
|
639
|
+
/** Schema for the invitation preview request */
|
|
640
|
+
const invitePreviewSchema = z.object({
|
|
641
|
+
email: z.string().email(),
|
|
642
|
+
givenName: z.string().min(1).max(255).optional(),
|
|
643
|
+
familyName: z.string().min(1).max(255).optional(),
|
|
644
|
+
personalMessage: z.string().max(500).optional(),
|
|
645
|
+
locale: z.string().max(10).optional(),
|
|
646
|
+
});
|
|
647
|
+
// ---------------------------------------------------------------------------
|
|
648
|
+
// Pre-assignment validation helpers
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
/**
|
|
651
|
+
* Validate that referenced roles and claims exist in the database.
|
|
652
|
+
*
|
|
653
|
+
* Checks that all applicationIds are valid within the org, all roleIds
|
|
654
|
+
* belong to their specified application, and all claimDefinitionIds
|
|
655
|
+
* belong to their specified application. Throws a ZodError-like 400
|
|
656
|
+
* on validation failure.
|
|
657
|
+
*
|
|
658
|
+
* @param orgId - Organization ID for scoping
|
|
659
|
+
* @param roles - Array of role pre-assignments to validate
|
|
660
|
+
* @param claims - Array of claim pre-assignments to validate
|
|
661
|
+
* @throws Error with 400 status if any reference is invalid
|
|
662
|
+
*/
|
|
663
|
+
async function validatePreAssignments(orgId, roles, claims) {
|
|
664
|
+
const pool = getPool();
|
|
665
|
+
const errors = [];
|
|
666
|
+
// Collect unique applicationIds from both roles and claims
|
|
667
|
+
const appIds = new Set();
|
|
668
|
+
roles?.forEach((r) => appIds.add(r.applicationId));
|
|
669
|
+
claims?.forEach((c) => appIds.add(c.applicationId));
|
|
670
|
+
// Verify all applications exist within the org
|
|
671
|
+
if (appIds.size > 0) {
|
|
672
|
+
const appResult = await pool.query(`SELECT id FROM applications WHERE id = ANY($1) AND organization_id = $2`, [Array.from(appIds), orgId]);
|
|
673
|
+
const foundIds = new Set(appResult.rows.map((r) => r.id));
|
|
674
|
+
for (const appId of appIds) {
|
|
675
|
+
if (!foundIds.has(appId)) {
|
|
676
|
+
errors.push(`Application ${appId} not found in this organization`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// Verify all roles exist within their application
|
|
681
|
+
if (roles?.length) {
|
|
682
|
+
for (const role of roles) {
|
|
683
|
+
const result = await pool.query(`SELECT id FROM roles WHERE id = $1 AND application_id = $2`, [role.roleId, role.applicationId]);
|
|
684
|
+
if (result.rows.length === 0) {
|
|
685
|
+
errors.push(`Role ${role.roleId} not found in application ${role.applicationId}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// Verify all claim definitions exist within their application
|
|
690
|
+
if (claims?.length) {
|
|
691
|
+
for (const claim of claims) {
|
|
692
|
+
const result = await pool.query(`SELECT id FROM claim_definitions WHERE id = $1 AND application_id = $2`, [claim.claimDefinitionId, claim.applicationId]);
|
|
693
|
+
if (result.rows.length === 0) {
|
|
694
|
+
errors.push(`Claim definition ${claim.claimDefinitionId} not found in application ${claim.applicationId}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (errors.length > 0) {
|
|
699
|
+
const err = new Error(`Pre-assignment validation failed: ${errors.join('; ')}`);
|
|
700
|
+
err.status = 400;
|
|
701
|
+
throw err;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// ---------------------------------------------------------------------------
|
|
705
|
+
// Standalone user router (non-org-scoped)
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
707
|
+
/**
|
|
708
|
+
* Create a standalone user admin router.
|
|
709
|
+
*
|
|
710
|
+
* Provides direct user access by userId at `/api/admin/users/:userId/*`
|
|
711
|
+
* without requiring the organizationId in the URL path. This is used by
|
|
712
|
+
* the Admin GUI SPA for user detail views and mutations where the user
|
|
713
|
+
* detail page only has the userId (from the URL) and not the orgId.
|
|
714
|
+
*
|
|
715
|
+
* The handlers delegate to the same user service functions as the
|
|
716
|
+
* org-scoped router. All routes require admin authentication and
|
|
717
|
+
* granular permissions.
|
|
718
|
+
*
|
|
719
|
+
* Prefix: /api/admin/users
|
|
720
|
+
*/
|
|
721
|
+
export function createStandaloneUserRouter() {
|
|
722
|
+
const router = new Router({ prefix: '/api/admin/users' });
|
|
723
|
+
router.use(requireAdminAuth());
|
|
724
|
+
// GET /:userId — Get user by ID
|
|
725
|
+
router.get('/:userId', requirePermission(ADMIN_PERMISSIONS.USER_READ), async (ctx) => {
|
|
726
|
+
const user = await userService.getUserById(ctx.params.userId);
|
|
727
|
+
if (!user) {
|
|
728
|
+
ctx.throw(404, 'User not found');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
setETagHeader(ctx, 'user', user.id, user.updatedAt);
|
|
732
|
+
ctx.body = { data: user };
|
|
733
|
+
});
|
|
734
|
+
// PUT /:userId — Update user profile
|
|
735
|
+
router.put('/:userId', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), async (ctx) => {
|
|
736
|
+
try {
|
|
737
|
+
const body = updateUserSchema.parse(ctx.request.body);
|
|
738
|
+
const current = await userService.getUserById(ctx.params.userId);
|
|
739
|
+
if (current && !checkIfMatch(ctx, 'user', current.id, current.updatedAt, current))
|
|
740
|
+
return;
|
|
741
|
+
const input = { ...body, address: body.address ?? undefined };
|
|
742
|
+
const user = await userService.updateUser(ctx.params.userId, input);
|
|
743
|
+
setETagHeader(ctx, 'user', user.id, user.updatedAt);
|
|
744
|
+
ctx.body = { data: user };
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
handleError(ctx, err);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
// POST /:userId/deactivate — Deactivate user
|
|
751
|
+
router.post('/:userId/deactivate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
|
|
752
|
+
try {
|
|
753
|
+
await guardSuperAdmin(ctx.params.userId, 'deactivate');
|
|
754
|
+
await userService.deactivateUser(ctx.params.userId);
|
|
755
|
+
ctx.status = 204;
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
handleError(ctx, err);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
// POST /:userId/reactivate — Reactivate user (inactive → active)
|
|
762
|
+
router.post('/:userId/reactivate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
|
|
763
|
+
try {
|
|
764
|
+
await userService.reactivateUser(ctx.params.userId);
|
|
765
|
+
ctx.status = 204;
|
|
766
|
+
}
|
|
767
|
+
catch (err) {
|
|
768
|
+
handleError(ctx, err);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
// POST /:userId/activate — Alias for reactivate (SPA compatibility)
|
|
772
|
+
router.post('/:userId/activate', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
|
|
773
|
+
try {
|
|
774
|
+
await userService.reactivateUser(ctx.params.userId);
|
|
775
|
+
ctx.status = 204;
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
handleError(ctx, err);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
// POST /:userId/suspend — Suspend user
|
|
782
|
+
router.post('/:userId/suspend', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
|
|
783
|
+
try {
|
|
784
|
+
await guardSuperAdmin(ctx.params.userId, 'suspend');
|
|
785
|
+
const body = suspendUserSchema.parse(ctx.request.body ?? {});
|
|
786
|
+
await userService.suspendUser(ctx.params.userId, body.reason);
|
|
787
|
+
ctx.status = 204;
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
handleError(ctx, err);
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
// POST /:userId/unsuspend — Unsuspend user
|
|
794
|
+
router.post('/:userId/unsuspend', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
|
|
795
|
+
try {
|
|
796
|
+
await userService.unsuspendUser(ctx.params.userId);
|
|
797
|
+
ctx.status = 204;
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
handleError(ctx, err);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
// POST /:userId/lock — Lock user
|
|
804
|
+
router.post('/:userId/lock', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
|
|
805
|
+
try {
|
|
806
|
+
await guardSuperAdmin(ctx.params.userId, 'lock');
|
|
807
|
+
const body = lockUserSchema.parse(ctx.request.body);
|
|
808
|
+
await userService.lockUser(ctx.params.userId, body.reason);
|
|
809
|
+
ctx.status = 204;
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
handleError(ctx, err);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
// POST /:userId/unlock — Unlock user
|
|
816
|
+
router.post('/:userId/unlock', requirePermission(ADMIN_PERMISSIONS.USER_SUSPEND), async (ctx) => {
|
|
817
|
+
try {
|
|
818
|
+
await userService.unlockUser(ctx.params.userId);
|
|
819
|
+
ctx.status = 204;
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
handleError(ctx, err);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
// POST /:userId/password — Set password
|
|
826
|
+
router.post('/:userId/password', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), async (ctx) => {
|
|
827
|
+
try {
|
|
828
|
+
const body = setPasswordSchema.parse(ctx.request.body);
|
|
829
|
+
await userService.setUserPassword(ctx.params.userId, body.password);
|
|
830
|
+
ctx.status = 204;
|
|
831
|
+
}
|
|
832
|
+
catch (err) {
|
|
833
|
+
handleError(ctx, err);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
// DELETE /:userId/password — Clear password
|
|
837
|
+
router.delete('/:userId/password', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), async (ctx) => {
|
|
838
|
+
try {
|
|
839
|
+
await userService.clearUserPassword(ctx.params.userId);
|
|
840
|
+
ctx.status = 204;
|
|
841
|
+
}
|
|
842
|
+
catch (err) {
|
|
843
|
+
handleError(ctx, err);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
// POST /:userId/verify-email — Mark email as verified
|
|
847
|
+
router.post('/:userId/verify-email', requirePermission(ADMIN_PERMISSIONS.USER_UPDATE), async (ctx) => {
|
|
848
|
+
try {
|
|
849
|
+
await userService.markEmailVerified(ctx.params.userId);
|
|
850
|
+
ctx.status = 204;
|
|
851
|
+
}
|
|
852
|
+
catch (err) {
|
|
853
|
+
handleError(ctx, err);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
// GET /:userId/history — Entity change history
|
|
857
|
+
router.get('/:userId/history', requirePermission(ADMIN_PERMISSIONS.USER_READ), async (ctx) => {
|
|
858
|
+
try {
|
|
859
|
+
const history = await getEntityHistory('user', ctx.params.userId);
|
|
860
|
+
ctx.body = { data: history };
|
|
861
|
+
}
|
|
862
|
+
catch (err) {
|
|
863
|
+
handleError(ctx, err);
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
return router;
|
|
867
|
+
}
|
|
868
|
+
//# sourceMappingURL=users.js.map
|