alepha 0.14.2 → 0.14.4
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/README.md +1 -1
- package/dist/api/audits/index.browser.js +5 -5
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +706 -785
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +13 -13
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +5 -5
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +58 -137
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +71 -71
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +5 -5
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +29 -108
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +10 -10
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.browser.js +10 -10
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +504 -171
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +12 -12
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.browser.js +163 -10
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +277 -351
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +196 -91
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/users/index.browser.js +19 -19
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +787 -852
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +827 -596
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.browser.js +6 -6
- package/dist/api/verifications/index.browser.js.map +1 -1
- package/dist/api/verifications/index.d.ts +128 -128
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js +6 -6
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/bin/index.d.ts +1 -2
- package/dist/bin/index.js +0 -1
- package/dist/bin/index.js.map +1 -1
- package/dist/cli/index.d.ts +252 -131
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +595 -395
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +46 -11
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +99 -19
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +40 -22
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +45 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +40 -22
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +40 -22
- package/dist/core/index.native.js.map +1 -1
- package/dist/fake/index.js +195 -168
- package/dist/fake/index.js.map +1 -1
- package/dist/file/index.d.ts +8 -0
- package/dist/file/index.d.ts.map +1 -1
- package/dist/file/index.js +3 -0
- package/dist/file/index.js.map +1 -1
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +12 -2
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.d.ts +59 -195
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +201 -430
- package/dist/orm/index.js.map +1 -1
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +171 -155
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +0 -1
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +12 -0
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +55 -2
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +6 -0
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +38 -1
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/core/index.browser.js +2 -2
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +10 -10
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +7 -4
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/links/index.browser.js +22 -6
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +46 -44
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +24 -41
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/static/index.js +4 -0
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +2 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +9 -5
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/vite/index.d.ts +101 -106
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +574 -503
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +7 -7
- package/package.json +7 -7
- package/src/api/audits/controllers/{AuditController.ts → AdminAuditController.ts} +5 -6
- package/src/api/audits/entities/audits.ts +5 -5
- package/src/api/audits/index.browser.ts +1 -1
- package/src/api/audits/index.ts +3 -3
- package/src/api/audits/primitives/$audit.spec.ts +276 -0
- package/src/api/audits/services/AuditService.spec.ts +495 -0
- package/src/api/files/__tests__/$bucket.spec.ts +91 -0
- package/src/api/files/controllers/AdminFileStatsController.spec.ts +166 -0
- package/src/api/files/controllers/{StorageStatsController.ts → AdminFileStatsController.ts} +2 -2
- package/src/api/files/controllers/FileController.spec.ts +558 -0
- package/src/api/files/controllers/FileController.ts +4 -5
- package/src/api/files/entities/files.ts +5 -5
- package/src/api/files/index.browser.ts +1 -1
- package/src/api/files/index.ts +4 -4
- package/src/api/files/jobs/FileJobs.spec.ts +52 -0
- package/src/api/files/services/FileService.spec.ts +109 -0
- package/src/api/jobs/__tests__/JobController.spec.ts +343 -0
- package/src/api/jobs/controllers/{JobController.ts → AdminJobController.ts} +2 -2
- package/src/api/jobs/entities/jobExecutions.ts +5 -5
- package/src/api/jobs/index.ts +3 -3
- package/src/api/jobs/primitives/$job.spec.ts +476 -0
- package/src/api/notifications/controllers/{NotificationController.ts → AdminNotificationController.ts} +4 -5
- package/src/api/notifications/entities/notifications.ts +5 -5
- package/src/api/notifications/index.browser.ts +1 -1
- package/src/api/notifications/index.ts +4 -4
- package/src/api/parameters/controllers/{ConfigController.ts → AdminConfigController.ts} +46 -107
- package/src/api/parameters/entities/parameters.ts +7 -17
- package/src/api/parameters/index.ts +3 -3
- package/src/api/parameters/primitives/$config.spec.ts +356 -0
- package/src/api/parameters/schemas/activateConfigBodySchema.ts +12 -0
- package/src/api/parameters/schemas/checkScheduledResponseSchema.ts +8 -0
- package/src/api/parameters/schemas/configCurrentResponseSchema.ts +13 -0
- package/src/api/parameters/schemas/configHistoryResponseSchema.ts +9 -0
- package/src/api/parameters/schemas/configNameParamSchema.ts +10 -0
- package/src/api/parameters/schemas/configNamesResponseSchema.ts +8 -0
- package/src/api/parameters/schemas/configTreeNodeSchema.ts +13 -0
- package/src/api/parameters/schemas/configVersionParamSchema.ts +9 -0
- package/src/api/parameters/schemas/configVersionResponseSchema.ts +9 -0
- package/src/api/parameters/schemas/configsByStatusResponseSchema.ts +9 -0
- package/src/api/parameters/schemas/createConfigVersionBodySchema.ts +24 -0
- package/src/api/parameters/schemas/index.ts +15 -0
- package/src/api/parameters/schemas/parameterResponseSchema.ts +26 -0
- package/src/api/parameters/schemas/parameterStatusSchema.ts +13 -0
- package/src/api/parameters/schemas/rollbackConfigBodySchema.ts +15 -0
- package/src/api/parameters/schemas/statusParamSchema.ts +9 -0
- package/src/api/users/__tests__/EmailVerification.spec.ts +369 -0
- package/src/api/users/__tests__/PasswordReset.spec.ts +550 -0
- package/src/api/users/controllers/AdminIdentityController.spec.ts +365 -0
- package/src/api/users/controllers/{IdentityController.ts → AdminIdentityController.ts} +3 -4
- package/src/api/users/controllers/AdminSessionController.spec.ts +274 -0
- package/src/api/users/controllers/{SessionController.ts → AdminSessionController.ts} +3 -4
- package/src/api/users/controllers/AdminUserController.spec.ts +372 -0
- package/src/api/users/controllers/AdminUserController.ts +116 -0
- package/src/api/users/controllers/UserController.ts +4 -107
- package/src/api/users/controllers/UserRealmController.ts +3 -0
- package/src/api/users/entities/identities.ts +6 -6
- package/src/api/users/entities/sessions.ts +6 -6
- package/src/api/users/entities/users.ts +9 -9
- package/src/api/users/index.ts +9 -6
- package/src/api/users/primitives/$userRealm.ts +13 -8
- package/src/api/users/services/CredentialService.spec.ts +509 -0
- package/src/api/users/services/CredentialService.ts +46 -0
- package/src/api/users/services/IdentityService.ts +15 -0
- package/src/api/users/services/RegistrationService.spec.ts +630 -0
- package/src/api/users/services/RegistrationService.ts +18 -0
- package/src/api/users/services/SessionService.spec.ts +301 -0
- package/src/api/users/services/SessionService.ts +110 -1
- package/src/api/users/services/UserService.ts +67 -2
- package/src/api/verifications/__tests__/CodeVerification.spec.ts +318 -0
- package/src/api/verifications/__tests__/LinkVerification.spec.ts +279 -0
- package/src/api/verifications/entities/verifications.ts +6 -6
- package/src/api/verifications/jobs/VerificationJobs.spec.ts +50 -0
- package/src/batch/__tests__/startup-buffering.spec.ts +458 -0
- package/src/batch/primitives/$batch.spec.ts +766 -0
- package/src/batch/providers/BatchProvider.spec.ts +786 -0
- package/src/bin/index.ts +0 -1
- package/src/bucket/__tests__/shared.ts +194 -0
- package/src/bucket/primitives/$bucket.spec.ts +104 -0
- package/src/bucket/providers/FileStorageProvider.spec.ts +13 -0
- package/src/bucket/providers/LocalFileStorageProvider.spec.ts +77 -0
- package/src/bucket/providers/MemoryFileStorageProvider.spec.ts +82 -0
- package/src/cache/core/__tests__/shared.ts +377 -0
- package/src/cache/core/primitives/$cache.spec.ts +111 -0
- package/src/cache/redis/__tests__/cache-redis.spec.ts +70 -0
- package/src/cli/apps/AlephaCli.ts +25 -6
- package/src/cli/atoms/buildOptions.ts +88 -0
- package/src/cli/commands/build.ts +32 -69
- package/src/cli/commands/db.ts +0 -4
- package/src/cli/commands/dev.ts +34 -10
- package/src/cli/commands/gen/changelog.spec.ts +315 -0
- package/src/cli/commands/{changelog.ts → gen/changelog.ts} +9 -9
- package/src/cli/commands/gen/env.ts +53 -0
- package/src/cli/commands/gen/openapi.ts +71 -0
- package/src/cli/commands/gen/resource.ts +15 -0
- package/src/cli/commands/gen.ts +24 -0
- package/src/cli/commands/init.ts +2 -1
- package/src/cli/commands/root.ts +12 -3
- package/src/cli/commands/test.ts +0 -1
- package/src/cli/commands/typecheck.ts +5 -0
- package/src/cli/commands/verify.ts +1 -1
- package/src/cli/defineConfig.ts +49 -7
- package/src/cli/index.ts +2 -2
- package/src/cli/services/AlephaCliUtils.ts +105 -55
- package/src/cli/services/GitMessageParser.ts +1 -1
- package/src/command/helpers/Asker.spec.ts +127 -0
- package/src/command/helpers/Runner.spec.ts +126 -0
- package/src/command/helpers/Runner.ts +1 -1
- package/src/command/primitives/$command.spec.ts +1588 -0
- package/src/command/primitives/$command.ts +0 -6
- package/src/command/providers/CliProvider.ts +75 -27
- package/src/core/Alepha.ts +87 -0
- package/src/core/__tests__/Alepha-emit.spec.ts +22 -0
- package/src/core/__tests__/Alepha-graph.spec.ts +93 -0
- package/src/core/__tests__/Alepha-has.spec.ts +41 -0
- package/src/core/__tests__/Alepha-inject.spec.ts +93 -0
- package/src/core/__tests__/Alepha-register.spec.ts +81 -0
- package/src/core/__tests__/Alepha-start.spec.ts +176 -0
- package/src/core/__tests__/Alepha-with.spec.ts +14 -0
- package/src/core/__tests__/TypeBox-usecases.spec.ts +35 -0
- package/src/core/__tests__/TypeBoxLocale.spec.ts +15 -0
- package/src/core/__tests__/descriptor.spec.ts +34 -0
- package/src/core/__tests__/fixtures/A.ts +5 -0
- package/src/core/__tests__/pagination.spec.ts +77 -0
- package/src/core/helpers/jsonSchemaToTypeBox.ts +2 -2
- package/src/core/primitives/$atom.spec.ts +43 -0
- package/src/core/primitives/$hook.spec.ts +130 -0
- package/src/core/primitives/$inject.spec.ts +175 -0
- package/src/core/primitives/$module.spec.ts +115 -0
- package/src/core/providers/CodecManager.spec.ts +740 -0
- package/src/core/providers/EventManager.spec.ts +762 -0
- package/src/core/providers/EventManager.ts +4 -0
- package/src/core/providers/StateManager.spec.ts +365 -0
- package/src/core/providers/TypeProvider.spec.ts +1607 -0
- package/src/core/providers/TypeProvider.ts +20 -26
- package/src/datetime/primitives/$interval.spec.ts +103 -0
- package/src/datetime/providers/DateTimeProvider.spec.ts +86 -0
- package/src/email/primitives/$email.spec.ts +175 -0
- package/src/email/providers/LocalEmailProvider.spec.ts +341 -0
- package/src/fake/__tests__/keyName.example.ts +40 -0
- package/src/fake/__tests__/keyName.spec.ts +152 -0
- package/src/fake/__tests__/module.example.ts +32 -0
- package/src/fake/providers/FakeProvider.spec.ts +438 -0
- package/src/file/providers/FileSystemProvider.ts +8 -0
- package/src/file/providers/NodeFileSystemProvider.spec.ts +418 -0
- package/src/file/providers/NodeFileSystemProvider.ts +5 -0
- package/src/file/services/FileDetector.spec.ts +591 -0
- package/src/lock/core/__tests__/shared.ts +190 -0
- package/src/lock/core/providers/MemoryLockProvider.spec.ts +25 -0
- package/src/lock/redis/providers/RedisLockProvider.spec.ts +25 -0
- package/src/logger/__tests__/SimpleFormatterProvider.spec.ts +109 -0
- package/src/logger/index.ts +15 -3
- package/src/logger/primitives/$logger.spec.ts +108 -0
- package/src/logger/services/Logger.spec.ts +295 -0
- package/src/mcp/__tests__/errors.spec.ts +175 -0
- package/src/mcp/__tests__/integration.spec.ts +450 -0
- package/src/mcp/helpers/jsonrpc.spec.ts +380 -0
- package/src/mcp/primitives/$prompt.spec.ts +468 -0
- package/src/mcp/primitives/$resource.spec.ts +390 -0
- package/src/mcp/primitives/$tool.spec.ts +406 -0
- package/src/mcp/providers/McpServerProvider.spec.ts +797 -0
- package/src/mcp/transports/StdioMcpTransport.ts +1 -1
- package/src/orm/__tests__/$repository-crud.spec.ts +276 -0
- package/src/orm/__tests__/$repository-hooks.spec.ts +325 -0
- package/src/orm/__tests__/$repository-orderBy.spec.ts +128 -0
- package/src/orm/__tests__/$repository-pagination-sort.spec.ts +149 -0
- package/src/orm/__tests__/$repository-save.spec.ts +37 -0
- package/src/orm/__tests__/ModelBuilder-integration.spec.ts +490 -0
- package/src/orm/__tests__/ModelBuilder-types.spec.ts +186 -0
- package/src/orm/__tests__/PostgresProvider.spec.ts +46 -0
- package/src/orm/__tests__/delete-returning.spec.ts +256 -0
- package/src/orm/__tests__/deletedAt.spec.ts +80 -0
- package/src/orm/__tests__/enums.spec.ts +315 -0
- package/src/orm/__tests__/execute.spec.ts +72 -0
- package/src/orm/__tests__/fixtures/bigEntitySchema.ts +65 -0
- package/src/orm/__tests__/fixtures/userEntitySchema.ts +27 -0
- package/src/orm/__tests__/joins.spec.ts +1114 -0
- package/src/orm/__tests__/page.spec.ts +287 -0
- package/src/orm/__tests__/primaryKey.spec.ts +87 -0
- package/src/orm/__tests__/query-date-encoding.spec.ts +402 -0
- package/src/orm/__tests__/ref-auto-onDelete.spec.ts +156 -0
- package/src/orm/__tests__/references.spec.ts +102 -0
- package/src/orm/__tests__/security.spec.ts +710 -0
- package/src/orm/__tests__/sqlite.spec.ts +111 -0
- package/src/orm/__tests__/string-operators.spec.ts +429 -0
- package/src/orm/__tests__/timestamps.spec.ts +388 -0
- package/src/orm/__tests__/validation.spec.ts +183 -0
- package/src/orm/__tests__/version.spec.ts +64 -0
- package/src/orm/helpers/parseQueryString.spec.ts +196 -0
- package/src/orm/index.ts +2 -8
- package/src/orm/primitives/$repository.spec.ts +137 -0
- package/src/orm/primitives/$sequence.spec.ts +29 -0
- package/src/orm/primitives/$transaction.spec.ts +82 -0
- package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -3
- package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +1 -1
- package/src/orm/providers/drivers/DatabaseProvider.ts +1 -1
- package/src/orm/providers/drivers/NodePostgresProvider.ts +3 -3
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +1 -1
- package/src/orm/providers/drivers/PglitePostgresProvider.ts +2 -2
- package/src/orm/services/ModelBuilder.spec.ts +575 -0
- package/src/orm/services/Repository.spec.ts +137 -0
- package/src/queue/core/__tests__/shared.ts +143 -0
- package/src/queue/core/providers/MemoryQueueProvider.spec.ts +23 -0
- package/src/queue/core/providers/WorkerProvider.spec.ts +394 -0
- package/src/queue/redis/providers/RedisQueueProvider.spec.ts +23 -0
- package/src/redis/__tests__/redis.spec.ts +58 -0
- package/src/retry/primitives/$retry.spec.ts +234 -0
- package/src/retry/providers/RetryProvider.spec.ts +438 -0
- package/src/router/__tests__/match.spec.ts +252 -0
- package/src/router/providers/RouterProvider.spec.ts +197 -0
- package/src/scheduler/__tests__/$scheduler-cron.spec.ts +25 -0
- package/src/scheduler/__tests__/$scheduler-interval.spec.ts +25 -0
- package/src/scheduler/__tests__/shared.ts +77 -0
- package/src/security/__tests__/bug-1-wildcard-after-start.spec.ts +229 -0
- package/src/security/__tests__/bug-2-password-validation.spec.ts +245 -0
- package/src/security/__tests__/bug-3-regex-vulnerability.spec.ts +407 -0
- package/src/security/__tests__/bug-4-oauth2-validation.spec.ts +439 -0
- package/src/security/__tests__/multi-layer-permissions.spec.ts +522 -0
- package/src/security/primitives/$permission.spec.ts +30 -0
- package/src/security/primitives/$permission.ts +2 -2
- package/src/security/primitives/$realm.spec.ts +101 -0
- package/src/security/primitives/$role.spec.ts +52 -0
- package/src/security/primitives/$serviceAccount.spec.ts +61 -0
- package/src/security/providers/SecurityProvider.spec.ts +350 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +0 -2
- package/src/server/cache/providers/ServerCacheProvider.spec.ts +1125 -0
- package/src/server/cache/providers/ServerCacheProvider.ts +94 -9
- package/src/server/compress/providers/ServerCompressProvider.spec.ts +31 -0
- package/src/server/compress/providers/ServerCompressProvider.ts +63 -2
- package/src/server/cookies/providers/ServerCookiesProvider.spec.ts +253 -0
- package/src/server/core/__tests__/ServerRouterProvider-getRoutes.spec.ts +334 -0
- package/src/server/core/__tests__/ServerRouterProvider-requestId.spec.ts +129 -0
- package/src/server/core/helpers/ServerReply.ts +2 -2
- package/src/server/core/primitives/$action.spec.ts +191 -0
- package/src/server/core/primitives/$route.spec.ts +65 -0
- package/src/server/core/providers/ServerBodyParserProvider.spec.ts +93 -0
- package/src/server/core/providers/ServerLoggerProvider.spec.ts +100 -0
- package/src/server/core/providers/ServerProvider.ts +14 -2
- package/src/server/core/services/HttpClient.spec.ts +123 -0
- package/src/server/core/services/UserAgentParser.spec.ts +111 -0
- package/src/server/cors/providers/ServerCorsProvider.spec.ts +481 -0
- package/src/server/health/providers/ServerHealthProvider.spec.ts +22 -0
- package/src/server/helmet/providers/ServerHelmetProvider.spec.ts +105 -0
- package/src/server/links/__tests__/$action.spec.ts +238 -0
- package/src/server/links/__tests__/fixtures/CrudApp.ts +122 -0
- package/src/server/links/__tests__/requestId.spec.ts +120 -0
- package/src/server/links/primitives/$remote.spec.ts +228 -0
- package/src/server/links/providers/LinkProvider.spec.ts +54 -0
- package/src/server/links/providers/LinkProvider.ts +49 -3
- package/src/server/links/providers/ServerLinksProvider.ts +1 -53
- package/src/server/links/schemas/apiLinksResponseSchema.ts +7 -0
- package/src/server/metrics/providers/ServerMetricsProvider.spec.ts +25 -0
- package/src/server/multipart/providers/ServerMultipartProvider.spec.ts +528 -0
- package/src/server/proxy/primitives/$proxy.spec.ts +87 -0
- package/src/server/rate-limit/__tests__/ActionRateLimit.spec.ts +211 -0
- package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +344 -0
- package/src/server/security/__tests__/BasicAuth.spec.ts +684 -0
- package/src/server/security/__tests__/ServerSecurityProvider-realm.spec.ts +388 -0
- package/src/server/security/providers/ServerSecurityProvider.spec.ts +123 -0
- package/src/server/static/primitives/$serve.spec.ts +193 -0
- package/src/server/static/providers/ServerStaticProvider.ts +10 -0
- package/src/server/swagger/__tests__/ui.spec.ts +52 -0
- package/src/server/swagger/primitives/$swagger.spec.ts +193 -0
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +19 -12
- package/src/sms/primitives/$sms.spec.ts +165 -0
- package/src/sms/providers/LocalSmsProvider.spec.ts +224 -0
- package/src/sms/providers/MemorySmsProvider.spec.ts +193 -0
- package/src/thread/primitives/$thread.spec.ts +186 -0
- package/src/topic/core/__tests__/shared.ts +144 -0
- package/src/topic/core/providers/MemoryTopicProvider.spec.ts +23 -0
- package/src/topic/redis/providers/RedisTopicProvider.spec.ts +23 -0
- package/src/vite/helpers/importViteReact.ts +13 -0
- package/src/vite/index.ts +1 -21
- package/src/vite/plugins/viteAlephaDev.ts +32 -5
- package/src/vite/plugins/viteAlephaSsrPreload.ts +222 -0
- package/src/vite/tasks/buildClient.ts +11 -0
- package/src/vite/tasks/buildServer.ts +47 -3
- package/src/vite/tasks/devServer.ts +69 -0
- package/src/vite/tasks/index.ts +2 -1
- package/src/vite/tasks/runAlepha.ts +7 -1
- package/src/websocket/__tests__/$websocket-new.spec.ts +195 -0
- package/src/websocket/primitives/$channel.spec.ts +30 -0
- package/src/cli/assets/viteConfigTs.ts +0 -14
- package/src/cli/commands/run.ts +0 -24
- package/src/vite/plugins/viteAlepha.ts +0 -37
- package/src/vite/plugins/viteAlephaBuild.ts +0 -281
package/dist/api/users/index.js
CHANGED
|
@@ -4,20 +4,19 @@ import { AlephaApiVerification } from "alepha/api/verifications";
|
|
|
4
4
|
import { AlephaEmail } from "alepha/email";
|
|
5
5
|
import { AlephaServerCompress } from "alepha/server/compress";
|
|
6
6
|
import { AlephaServerHelmet } from "alepha/server/helmet";
|
|
7
|
-
import { $entity, $repository, pageQuerySchema, parseQueryString, pg } from "alepha/orm";
|
|
8
7
|
import { $action, BadRequestError, ConflictError, HttpError, UnauthorizedError, okSchema } from "alepha/server";
|
|
8
|
+
import { $entity, $repository, db, pageQuerySchema, parseQueryString } from "alepha/orm";
|
|
9
|
+
import { AlephaApiAudits, AuditService } from "alepha/api/audits";
|
|
9
10
|
import { $logger } from "alepha/logger";
|
|
10
11
|
import { $bucket } from "alepha/bucket";
|
|
12
|
+
import { $client } from "alepha/server/links";
|
|
11
13
|
import { randomInt, randomUUID } from "node:crypto";
|
|
12
14
|
import { $cache } from "alepha/cache";
|
|
13
15
|
import { DateTimeProvider } from "alepha/datetime";
|
|
14
16
|
import { $realm, CryptoProvider, InvalidCredentialsError, SecurityProvider } from "alepha/security";
|
|
15
|
-
import { $client } from "alepha/server/links";
|
|
16
17
|
import { $authCredentials, $authGithub, $authGoogle, ServerAuthProvider, authenticationProviderSchema } from "alepha/server/auth";
|
|
17
18
|
import { FileSystemProvider } from "alepha/file";
|
|
18
|
-
import { AlephaApiAudits } from "alepha/api/audits";
|
|
19
19
|
import { AlephaApiFiles } from "alepha/api/files";
|
|
20
|
-
import { AlephaApiJobs } from "alepha/api/jobs";
|
|
21
20
|
|
|
22
21
|
//#region ../../src/api/users/schemas/identityQuerySchema.ts
|
|
23
22
|
const identityQuerySchema = t.extend(pageQuerySchema, {
|
|
@@ -31,11 +30,11 @@ const DEFAULT_USER_REALM_NAME = "default";
|
|
|
31
30
|
const users = $entity({
|
|
32
31
|
name: "users",
|
|
33
32
|
schema: t.object({
|
|
34
|
-
id:
|
|
35
|
-
version:
|
|
36
|
-
createdAt:
|
|
37
|
-
updatedAt:
|
|
38
|
-
realm:
|
|
33
|
+
id: db.primaryKey(t.uuid()),
|
|
34
|
+
version: db.version(),
|
|
35
|
+
createdAt: db.createdAt(),
|
|
36
|
+
updatedAt: db.updatedAt(),
|
|
37
|
+
realm: db.default(t.text(), DEFAULT_USER_REALM_NAME),
|
|
39
38
|
username: t.optional(t.shortText({
|
|
40
39
|
minLength: 3,
|
|
41
40
|
maxLength: 50,
|
|
@@ -43,12 +42,12 @@ const users = $entity({
|
|
|
43
42
|
})),
|
|
44
43
|
email: t.optional(t.string({ format: "email" })),
|
|
45
44
|
phoneNumber: t.optional(t.e164()),
|
|
46
|
-
roles:
|
|
45
|
+
roles: db.default(t.array(t.string()), []),
|
|
47
46
|
firstName: t.optional(t.string()),
|
|
48
47
|
lastName: t.optional(t.string()),
|
|
49
48
|
picture: t.optional(t.string()),
|
|
50
|
-
enabled:
|
|
51
|
-
emailVerified:
|
|
49
|
+
enabled: db.default(t.boolean(), true),
|
|
50
|
+
emailVerified: db.default(t.boolean(), false)
|
|
52
51
|
}),
|
|
53
52
|
indexes: [
|
|
54
53
|
{
|
|
@@ -71,11 +70,11 @@ const users = $entity({
|
|
|
71
70
|
const identities = $entity({
|
|
72
71
|
name: "identities",
|
|
73
72
|
schema: t.object({
|
|
74
|
-
id:
|
|
75
|
-
version:
|
|
76
|
-
createdAt:
|
|
77
|
-
updatedAt:
|
|
78
|
-
userId:
|
|
73
|
+
id: db.primaryKey(t.uuid()),
|
|
74
|
+
version: db.version(),
|
|
75
|
+
createdAt: db.createdAt(),
|
|
76
|
+
updatedAt: db.updatedAt(),
|
|
77
|
+
userId: db.ref(t.uuid(), () => users.cols.id),
|
|
79
78
|
password: t.optional(t.text()),
|
|
80
79
|
provider: t.text(),
|
|
81
80
|
providerUserId: t.optional(t.text()),
|
|
@@ -147,12 +146,12 @@ const realmAuthSettingsAtom = $atom({
|
|
|
147
146
|
const sessions = $entity({
|
|
148
147
|
name: "sessions",
|
|
149
148
|
schema: t.object({
|
|
150
|
-
id:
|
|
151
|
-
version:
|
|
152
|
-
createdAt:
|
|
153
|
-
updatedAt:
|
|
149
|
+
id: db.primaryKey(t.uuid()),
|
|
150
|
+
version: db.version(),
|
|
151
|
+
createdAt: db.createdAt(),
|
|
152
|
+
updatedAt: db.updatedAt(),
|
|
154
153
|
refreshToken: t.uuid(),
|
|
155
|
-
userId:
|
|
154
|
+
userId: db.ref(t.uuid(), () => users.cols.id),
|
|
156
155
|
expiresAt: t.datetime(),
|
|
157
156
|
ip: t.optional(t.text()),
|
|
158
157
|
userAgent: t.optional(t.object({
|
|
@@ -242,6 +241,7 @@ var UserRealmProvider = class {
|
|
|
242
241
|
var IdentityService = class {
|
|
243
242
|
log = $logger();
|
|
244
243
|
userRealmProvider = $inject(UserRealmProvider);
|
|
244
|
+
auditService = $inject(AuditService);
|
|
245
245
|
identities(userRealmName) {
|
|
246
246
|
return this.userRealmProvider.identityRepository(userRealmName);
|
|
247
247
|
}
|
|
@@ -295,14 +295,25 @@ var IdentityService = class {
|
|
|
295
295
|
provider: identity.provider,
|
|
296
296
|
userId: identity.userId
|
|
297
297
|
});
|
|
298
|
+
const realm = this.userRealmProvider.getRealm(userRealmName);
|
|
299
|
+
await this.auditService.recordUser("update", {
|
|
300
|
+
userRealm: realm.name,
|
|
301
|
+
resourceId: identity.userId,
|
|
302
|
+
description: `Identity provider disconnected: ${identity.provider}`,
|
|
303
|
+
metadata: {
|
|
304
|
+
identityId: id,
|
|
305
|
+
provider: identity.provider,
|
|
306
|
+
userId: identity.userId
|
|
307
|
+
}
|
|
308
|
+
});
|
|
298
309
|
}
|
|
299
310
|
};
|
|
300
311
|
|
|
301
312
|
//#endregion
|
|
302
|
-
//#region ../../src/api/users/controllers/
|
|
303
|
-
var
|
|
313
|
+
//#region ../../src/api/users/controllers/AdminIdentityController.ts
|
|
314
|
+
var AdminIdentityController = class {
|
|
304
315
|
url = "/identities";
|
|
305
|
-
group = "identities";
|
|
316
|
+
group = "admin:identities";
|
|
306
317
|
identityService = $inject(IdentityService);
|
|
307
318
|
/**
|
|
308
319
|
* Find identities with pagination and filtering.
|
|
@@ -313,7 +324,7 @@ var IdentityController = class {
|
|
|
313
324
|
description: "Find identities with pagination and filtering",
|
|
314
325
|
schema: {
|
|
315
326
|
query: t.extend(identityQuerySchema, { userRealmName: t.optional(t.string()) }),
|
|
316
|
-
response:
|
|
327
|
+
response: t.page(identityResourceSchema)
|
|
317
328
|
},
|
|
318
329
|
handler: ({ query }) => {
|
|
319
330
|
const { userRealmName, ...q } = query;
|
|
@@ -439,10 +450,10 @@ var SessionCrudService = class {
|
|
|
439
450
|
};
|
|
440
451
|
|
|
441
452
|
//#endregion
|
|
442
|
-
//#region ../../src/api/users/controllers/
|
|
443
|
-
var
|
|
453
|
+
//#region ../../src/api/users/controllers/AdminSessionController.ts
|
|
454
|
+
var AdminSessionController = class {
|
|
444
455
|
url = "/sessions";
|
|
445
|
-
group = "sessions";
|
|
456
|
+
group = "admin:sessions";
|
|
446
457
|
sessionService = $inject(SessionCrudService);
|
|
447
458
|
/**
|
|
448
459
|
* Find sessions with pagination and filtering.
|
|
@@ -453,7 +464,7 @@ var SessionController = class {
|
|
|
453
464
|
description: "Find sessions with pagination and filtering",
|
|
454
465
|
schema: {
|
|
455
466
|
query: t.extend(sessionQuerySchema, { userRealmName: t.optional(t.string()) }),
|
|
456
|
-
response:
|
|
467
|
+
response: t.page(sessionResourceSchema)
|
|
457
468
|
},
|
|
458
469
|
handler: ({ query }) => {
|
|
459
470
|
const { userRealmName, ...q } = query;
|
|
@@ -497,92 +508,10 @@ var SessionController = class {
|
|
|
497
508
|
});
|
|
498
509
|
};
|
|
499
510
|
|
|
500
|
-
//#endregion
|
|
501
|
-
//#region ../../src/api/users/schemas/completePasswordResetRequestSchema.ts
|
|
502
|
-
/**
|
|
503
|
-
* Request schema for completing a password reset.
|
|
504
|
-
*
|
|
505
|
-
* Requires the intent ID from Phase 1, the verification code,
|
|
506
|
-
* and the new password.
|
|
507
|
-
*/
|
|
508
|
-
const completePasswordResetRequestSchema = t.object({
|
|
509
|
-
intentId: t.uuid({ description: "The intent ID from createPasswordResetIntent" }),
|
|
510
|
-
code: t.string({ description: "6-digit verification code sent via email" }),
|
|
511
|
-
newPassword: t.string({
|
|
512
|
-
minLength: 8,
|
|
513
|
-
description: "New password (minimum 8 characters)"
|
|
514
|
-
})
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
//#endregion
|
|
518
|
-
//#region ../../src/api/users/schemas/completeRegistrationRequestSchema.ts
|
|
519
|
-
const completeRegistrationRequestSchema = t.object({
|
|
520
|
-
intentId: t.uuid({ description: "The registration intent ID from the first phase" }),
|
|
521
|
-
emailCode: t.optional(t.string({ description: "Email verification code (if email verification required)" })),
|
|
522
|
-
phoneCode: t.optional(t.string({ description: "Phone verification code (if phone verification required)" })),
|
|
523
|
-
captchaToken: t.optional(t.string({ description: "Captcha token (if captcha required)" }))
|
|
524
|
-
});
|
|
525
|
-
|
|
526
511
|
//#endregion
|
|
527
512
|
//#region ../../src/api/users/schemas/createUserSchema.ts
|
|
528
513
|
const createUserSchema = t.omit(users.insertSchema, ["realm"]);
|
|
529
514
|
|
|
530
|
-
//#endregion
|
|
531
|
-
//#region ../../src/api/users/schemas/passwordResetIntentResponseSchema.ts
|
|
532
|
-
/**
|
|
533
|
-
* Response schema for password reset intent creation.
|
|
534
|
-
*
|
|
535
|
-
* Contains the intent ID needed for Phase 2 completion,
|
|
536
|
-
* along with expiration time.
|
|
537
|
-
*/
|
|
538
|
-
const passwordResetIntentResponseSchema = t.object({
|
|
539
|
-
intentId: t.uuid({ description: "Unique identifier for this password reset intent" }),
|
|
540
|
-
expiresAt: t.datetime({ description: "ISO timestamp when this intent expires" })
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
//#endregion
|
|
544
|
-
//#region ../../src/api/users/schemas/registerQuerySchema.ts
|
|
545
|
-
/**
|
|
546
|
-
* Schema for user registration query parameters.
|
|
547
|
-
* Allows specifying a custom user realm.
|
|
548
|
-
*/
|
|
549
|
-
const registerQuerySchema = t.object({ userRealmName: t.optional(t.text({ description: "The user realm to register the user in (defaults to 'default')" })) });
|
|
550
|
-
|
|
551
|
-
//#endregion
|
|
552
|
-
//#region ../../src/api/users/schemas/registerRequestSchema.ts
|
|
553
|
-
/**
|
|
554
|
-
* Schema for user registration request body.
|
|
555
|
-
* Password is always required, other fields depend on realm settings.
|
|
556
|
-
*/
|
|
557
|
-
const registerRequestSchema = t.object({
|
|
558
|
-
password: t.string({
|
|
559
|
-
minLength: 8,
|
|
560
|
-
description: "Password for the account"
|
|
561
|
-
}),
|
|
562
|
-
username: t.optional(t.string({
|
|
563
|
-
minLength: 3,
|
|
564
|
-
description: "Unique username for the account"
|
|
565
|
-
})),
|
|
566
|
-
email: t.optional(t.string({
|
|
567
|
-
format: "email",
|
|
568
|
-
description: "User's email address"
|
|
569
|
-
})),
|
|
570
|
-
phoneNumber: t.optional(t.string({ description: "User's phone number" })),
|
|
571
|
-
firstName: t.optional(t.string({ description: "User's first name" })),
|
|
572
|
-
lastName: t.optional(t.string({ description: "User's last name" })),
|
|
573
|
-
picture: t.optional(t.string({ description: "User's profile picture URL" }))
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
//#endregion
|
|
577
|
-
//#region ../../src/api/users/schemas/registrationIntentResponseSchema.ts
|
|
578
|
-
const registrationIntentResponseSchema = t.object({
|
|
579
|
-
intentId: t.uuid({ description: "Unique identifier for the registration intent" }),
|
|
580
|
-
expectCaptcha: t.boolean({ description: "Whether captcha verification is required" }),
|
|
581
|
-
expectEmailVerification: t.boolean({ description: "Whether email verification is required" }),
|
|
582
|
-
expectPhoneVerification: t.boolean({ description: "Whether phone verification is required" }),
|
|
583
|
-
expiresAt: t.datetime({ description: "When the registration intent expires" })
|
|
584
|
-
});
|
|
585
|
-
|
|
586
515
|
//#endregion
|
|
587
516
|
//#region ../../src/api/users/schemas/updateUserSchema.ts
|
|
588
517
|
const updateUserSchema = t.partial(t.omit(users.insertSchema, [
|
|
@@ -738,136 +667,578 @@ var UserNotifications = class {
|
|
|
738
667
|
};
|
|
739
668
|
|
|
740
669
|
//#endregion
|
|
741
|
-
//#region ../../src/api/users/services/
|
|
742
|
-
|
|
743
|
-
var CredentialService = class {
|
|
670
|
+
//#region ../../src/api/users/services/UserService.ts
|
|
671
|
+
var UserService = class {
|
|
744
672
|
log = $logger();
|
|
745
|
-
cryptoProvider = $inject(CryptoProvider);
|
|
746
|
-
dateTimeProvider = $inject(DateTimeProvider);
|
|
747
673
|
verificationController = $client();
|
|
748
674
|
userNotifications = $inject(UserNotifications);
|
|
749
675
|
userRealmProvider = $inject(UserRealmProvider);
|
|
750
|
-
|
|
751
|
-
name: "password-reset-intents",
|
|
752
|
-
ttl: [INTENT_TTL_MINUTES$1, "minutes"]
|
|
753
|
-
});
|
|
676
|
+
auditService = $inject(AuditService);
|
|
754
677
|
users(userRealmName) {
|
|
755
678
|
return this.userRealmProvider.userRepository(userRealmName);
|
|
756
679
|
}
|
|
757
|
-
sessions(userRealmName) {
|
|
758
|
-
return this.userRealmProvider.sessionRepository(userRealmName);
|
|
759
|
-
}
|
|
760
|
-
identities(userRealmName) {
|
|
761
|
-
return this.userRealmProvider.identityRepository(userRealmName);
|
|
762
|
-
}
|
|
763
680
|
/**
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* @param email - User's email address
|
|
770
|
-
* @param userRealmName - Optional realm name
|
|
771
|
-
* @returns Intent response with intentId and expiration (always returns for security)
|
|
681
|
+
* Request email verification for a user.
|
|
682
|
+
* @param email - The email address to verify.
|
|
683
|
+
* @param userRealmName - Optional realm name.
|
|
684
|
+
* @param method - The verification method: "code" (default) or "link".
|
|
685
|
+
* @param verifyUrl - Base URL for verification link (required when method is "link").
|
|
772
686
|
*/
|
|
773
|
-
async
|
|
774
|
-
this.log.trace("
|
|
687
|
+
async requestEmailVerification(email, userRealmName, method = "code", verifyUrl) {
|
|
688
|
+
this.log.trace("Requesting email verification", {
|
|
775
689
|
email,
|
|
776
|
-
userRealmName
|
|
690
|
+
userRealmName,
|
|
691
|
+
method
|
|
777
692
|
});
|
|
778
|
-
const intentId = randomUUID();
|
|
779
|
-
const expiresAt = this.dateTimeProvider.now().add(INTENT_TTL_MINUTES$1, "minutes").toISOString();
|
|
780
693
|
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } }).catch(() => void 0);
|
|
781
694
|
if (!user) {
|
|
782
|
-
this.log.debug("
|
|
783
|
-
return
|
|
784
|
-
intentId,
|
|
785
|
-
expiresAt
|
|
786
|
-
};
|
|
695
|
+
this.log.debug("Email verification requested for non-existent user", { email });
|
|
696
|
+
return true;
|
|
787
697
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return {
|
|
795
|
-
intentId,
|
|
796
|
-
expiresAt
|
|
797
|
-
};
|
|
698
|
+
if (user.emailVerified) {
|
|
699
|
+
this.log.debug("Email verification requested for already verified user", {
|
|
700
|
+
email,
|
|
701
|
+
userId: user.id
|
|
702
|
+
});
|
|
703
|
+
return true;
|
|
798
704
|
}
|
|
799
705
|
try {
|
|
800
706
|
const verification = await this.verificationController.requestVerificationCode({
|
|
801
|
-
params: { type:
|
|
707
|
+
params: { type: method },
|
|
802
708
|
body: { target: email }
|
|
803
709
|
});
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
710
|
+
if (method === "link") {
|
|
711
|
+
const url = new URL(verifyUrl || "/verify-email", "http://localhost");
|
|
712
|
+
url.searchParams.set("email", email);
|
|
713
|
+
url.searchParams.set("token", verification.token);
|
|
714
|
+
const fullVerifyUrl = verifyUrl ? `${verifyUrl}${url.search}` : url.pathname + url.search;
|
|
715
|
+
await this.userNotifications.emailVerificationLink.push({
|
|
716
|
+
contact: email,
|
|
717
|
+
variables: {
|
|
718
|
+
email,
|
|
719
|
+
verifyUrl: fullVerifyUrl,
|
|
720
|
+
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
this.log.debug("Email verification link sent", {
|
|
807
724
|
email,
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
725
|
+
userId: user.id
|
|
726
|
+
});
|
|
727
|
+
} else {
|
|
728
|
+
await this.userNotifications.emailVerification.push({
|
|
729
|
+
contact: email,
|
|
730
|
+
variables: {
|
|
731
|
+
email,
|
|
732
|
+
code: verification.token,
|
|
733
|
+
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
this.log.debug("Email verification code sent", {
|
|
737
|
+
email,
|
|
738
|
+
userId: user.id
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this.log.warn("Failed to send email verification", {
|
|
813
743
|
email,
|
|
814
|
-
|
|
815
|
-
identityId: identity.id,
|
|
816
|
-
realmName: userRealmName,
|
|
817
|
-
expiresAt
|
|
818
|
-
};
|
|
819
|
-
await this.intentCache.set(intentId, intent);
|
|
820
|
-
this.log.info("Password reset intent created", {
|
|
821
|
-
intentId,
|
|
822
|
-
userId: user.id,
|
|
823
|
-
email
|
|
744
|
+
error
|
|
824
745
|
});
|
|
825
|
-
} catch (error) {
|
|
826
|
-
this.log.warn("Failed to create password reset verification", error);
|
|
827
746
|
}
|
|
828
|
-
return
|
|
829
|
-
intentId,
|
|
830
|
-
expiresAt
|
|
831
|
-
};
|
|
747
|
+
return true;
|
|
832
748
|
}
|
|
833
749
|
/**
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
* Validates the verification code, updates the password,
|
|
837
|
-
* and invalidates all existing sessions.
|
|
838
|
-
*
|
|
839
|
-
* @param body - Request body with intentId, code, and newPassword
|
|
750
|
+
* Verify a user's email using a valid verification token.
|
|
751
|
+
* Supports both code (6-digit) and link (UUID) verification tokens.
|
|
840
752
|
*/
|
|
841
|
-
async
|
|
842
|
-
this.log.trace("
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
status: 410,
|
|
848
|
-
message: "Invalid or expired password reset intent"
|
|
849
|
-
});
|
|
850
|
-
}
|
|
753
|
+
async verifyEmail(email, token, userRealmName) {
|
|
754
|
+
this.log.trace("Verifying email", {
|
|
755
|
+
email,
|
|
756
|
+
userRealmName
|
|
757
|
+
});
|
|
758
|
+
const type = /^\d{6}$/.test(token) ? "code" : "link";
|
|
851
759
|
if ((await this.verificationController.validateVerificationCode({
|
|
852
|
-
params: { type
|
|
760
|
+
params: { type },
|
|
853
761
|
body: {
|
|
854
|
-
target:
|
|
855
|
-
token
|
|
762
|
+
target: email,
|
|
763
|
+
token
|
|
856
764
|
}
|
|
857
765
|
}).catch(() => {
|
|
858
|
-
this.log.warn("Invalid verification
|
|
859
|
-
|
|
860
|
-
|
|
766
|
+
this.log.warn("Invalid email verification token", {
|
|
767
|
+
email,
|
|
768
|
+
type
|
|
861
769
|
});
|
|
862
|
-
throw new BadRequestError("Invalid or expired verification
|
|
770
|
+
throw new BadRequestError("Invalid or expired verification token");
|
|
863
771
|
})).alreadyVerified) {
|
|
864
|
-
this.log.warn("
|
|
865
|
-
|
|
866
|
-
email: intent.email
|
|
867
|
-
});
|
|
868
|
-
throw new BadRequestError("Verification code has already been used");
|
|
772
|
+
this.log.warn("Email verification token already used", { email });
|
|
773
|
+
throw new BadRequestError("Invalid or expired verification token");
|
|
869
774
|
}
|
|
870
|
-
await this.
|
|
775
|
+
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } });
|
|
776
|
+
await this.users(userRealmName).updateById(user.id, { emailVerified: true });
|
|
777
|
+
this.log.info("Email verified", {
|
|
778
|
+
email,
|
|
779
|
+
userId: user.id,
|
|
780
|
+
type
|
|
781
|
+
});
|
|
782
|
+
const realm = this.userRealmProvider.getRealm(userRealmName);
|
|
783
|
+
await this.auditService.recordUser("update", {
|
|
784
|
+
userId: user.id,
|
|
785
|
+
userEmail: email,
|
|
786
|
+
userRealm: realm.name,
|
|
787
|
+
resourceId: user.id,
|
|
788
|
+
description: "Email verified",
|
|
789
|
+
metadata: {
|
|
790
|
+
email,
|
|
791
|
+
verificationType: type
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Check if an email is verified.
|
|
797
|
+
*/
|
|
798
|
+
async isEmailVerified(email, userRealmName) {
|
|
799
|
+
this.log.trace("Checking if email is verified", {
|
|
800
|
+
email,
|
|
801
|
+
userRealmName
|
|
802
|
+
});
|
|
803
|
+
return (await this.users(userRealmName).findOne({ where: { email: { eq: email } } }).catch(() => void 0))?.emailVerified ?? false;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Find users with pagination and filtering.
|
|
807
|
+
*/
|
|
808
|
+
async findUsers(q = {}, userRealmName) {
|
|
809
|
+
this.log.trace("Finding users", {
|
|
810
|
+
query: q,
|
|
811
|
+
userRealmName
|
|
812
|
+
});
|
|
813
|
+
q.sort ??= "-createdAt";
|
|
814
|
+
const where = this.users(userRealmName).createQueryWhere();
|
|
815
|
+
if (q.email) where.email = { like: q.email };
|
|
816
|
+
if (q.enabled !== void 0) where.enabled = { eq: q.enabled };
|
|
817
|
+
if (q.emailVerified !== void 0) where.emailVerified = { eq: q.emailVerified };
|
|
818
|
+
if (q.roles) where.roles = { arrayContains: q.roles };
|
|
819
|
+
if (q.query) Object.assign(where, parseQueryString(q.query));
|
|
820
|
+
const result = await this.users(userRealmName).paginate(q, { where }, { count: true });
|
|
821
|
+
this.log.debug("Users found", {
|
|
822
|
+
count: result.content.length,
|
|
823
|
+
total: result.page.totalElements
|
|
824
|
+
});
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Get a user by ID.
|
|
829
|
+
*/
|
|
830
|
+
async getUserById(id, userRealmName) {
|
|
831
|
+
this.log.trace("Getting user by ID", {
|
|
832
|
+
id,
|
|
833
|
+
userRealmName
|
|
834
|
+
});
|
|
835
|
+
return await this.users(userRealmName).findById(id);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Create a new user.
|
|
839
|
+
*/
|
|
840
|
+
async createUser(data, userRealmName) {
|
|
841
|
+
this.log.trace("Creating user", {
|
|
842
|
+
username: data.username,
|
|
843
|
+
email: data.email,
|
|
844
|
+
userRealmName
|
|
845
|
+
});
|
|
846
|
+
const realm = this.userRealmProvider.getRealm(userRealmName);
|
|
847
|
+
if (data.username) {
|
|
848
|
+
if (await this.users(userRealmName).findOne({ where: { username: { eq: data.username } } }).catch(() => void 0)) {
|
|
849
|
+
this.log.debug("Username already taken", { username: data.username });
|
|
850
|
+
throw new BadRequestError("User with this username already exists");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (data.email) {
|
|
854
|
+
if (await this.users(userRealmName).findOne({ where: { email: { eq: data.email } } }).catch(() => void 0)) {
|
|
855
|
+
this.log.debug("Email already taken", { email: data.email });
|
|
856
|
+
throw new BadRequestError("User with this email already exists");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (data.phoneNumber) {
|
|
860
|
+
if (await this.users(userRealmName).findOne({ where: { phoneNumber: { eq: data.phoneNumber } } }).catch(() => void 0)) {
|
|
861
|
+
this.log.debug("Phone number already taken", { phoneNumber: data.phoneNumber });
|
|
862
|
+
throw new BadRequestError("User with this phone number already exists");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const user = await this.users(userRealmName).create({
|
|
866
|
+
...data,
|
|
867
|
+
roles: data.roles ?? ["user"],
|
|
868
|
+
realm: realm.name
|
|
869
|
+
});
|
|
870
|
+
this.log.info("User created", {
|
|
871
|
+
userId: user.id,
|
|
872
|
+
username: user.username,
|
|
873
|
+
email: user.email
|
|
874
|
+
});
|
|
875
|
+
await this.auditService.recordUser("create", {
|
|
876
|
+
userRealm: realm.name,
|
|
877
|
+
resourceId: user.id,
|
|
878
|
+
description: "User created",
|
|
879
|
+
metadata: {
|
|
880
|
+
username: user.username,
|
|
881
|
+
email: user.email,
|
|
882
|
+
roles: user.roles
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
return user;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Update an existing user.
|
|
889
|
+
*/
|
|
890
|
+
async updateUser(id, data, userRealmName) {
|
|
891
|
+
this.log.trace("Updating user", {
|
|
892
|
+
id,
|
|
893
|
+
userRealmName
|
|
894
|
+
});
|
|
895
|
+
const before = await this.getUserById(id, userRealmName);
|
|
896
|
+
const user = await this.users(userRealmName).updateById(id, data);
|
|
897
|
+
this.log.debug("User updated", { userId: id });
|
|
898
|
+
const realm = this.userRealmProvider.getRealm(userRealmName);
|
|
899
|
+
const changes = {};
|
|
900
|
+
for (const key of Object.keys(data)) if (data[key] !== void 0 && before[key] !== data[key]) changes[key] = {
|
|
901
|
+
from: before[key],
|
|
902
|
+
to: data[key]
|
|
903
|
+
};
|
|
904
|
+
const isRoleChange = data.roles !== void 0 && JSON.stringify(before.roles) !== JSON.stringify(data.roles);
|
|
905
|
+
await this.auditService.recordUser(isRoleChange ? "role_change" : "update", {
|
|
906
|
+
userRealm: realm.name,
|
|
907
|
+
resourceId: user.id,
|
|
908
|
+
description: isRoleChange ? "User roles changed" : `User updated: ${Object.keys(changes).join(", ")}`,
|
|
909
|
+
metadata: { changes }
|
|
910
|
+
});
|
|
911
|
+
return user;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Delete a user by ID.
|
|
915
|
+
*/
|
|
916
|
+
async deleteUser(id, userRealmName) {
|
|
917
|
+
this.log.trace("Deleting user", {
|
|
918
|
+
id,
|
|
919
|
+
userRealmName
|
|
920
|
+
});
|
|
921
|
+
const user = await this.getUserById(id, userRealmName);
|
|
922
|
+
await this.users(userRealmName).deleteById(id);
|
|
923
|
+
this.log.info("User deleted", { userId: id });
|
|
924
|
+
const realm = this.userRealmProvider.getRealm(userRealmName);
|
|
925
|
+
await this.auditService.recordUser("delete", {
|
|
926
|
+
userRealm: realm.name,
|
|
927
|
+
resourceId: id,
|
|
928
|
+
severity: "warning",
|
|
929
|
+
description: "User deleted",
|
|
930
|
+
metadata: {
|
|
931
|
+
username: user.username,
|
|
932
|
+
email: user.email
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
//#endregion
|
|
939
|
+
//#region ../../src/api/users/controllers/AdminUserController.ts
|
|
940
|
+
var AdminUserController = class {
|
|
941
|
+
url = "/users";
|
|
942
|
+
group = "admin:users";
|
|
943
|
+
userService = $inject(UserService);
|
|
944
|
+
/**
|
|
945
|
+
* Find users with pagination and filtering.
|
|
946
|
+
*/
|
|
947
|
+
findUsers = $action({
|
|
948
|
+
path: this.url,
|
|
949
|
+
group: this.group,
|
|
950
|
+
description: "Find users with pagination and filtering",
|
|
951
|
+
schema: {
|
|
952
|
+
query: t.extend(userQuerySchema, { userRealmName: t.optional(t.string()) }),
|
|
953
|
+
response: t.page(userResourceSchema)
|
|
954
|
+
},
|
|
955
|
+
handler: ({ query }) => {
|
|
956
|
+
const { userRealmName, ...q } = query;
|
|
957
|
+
return this.userService.findUsers(q, userRealmName);
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
/**
|
|
961
|
+
* Get a user by ID.
|
|
962
|
+
*/
|
|
963
|
+
getUser = $action({
|
|
964
|
+
path: `${this.url}/:id`,
|
|
965
|
+
group: this.group,
|
|
966
|
+
description: "Get a user by ID",
|
|
967
|
+
schema: {
|
|
968
|
+
params: t.object({ id: t.uuid() }),
|
|
969
|
+
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
970
|
+
response: userResourceSchema
|
|
971
|
+
},
|
|
972
|
+
handler: ({ params, query }) => this.userService.getUserById(params.id, query.userRealmName)
|
|
973
|
+
});
|
|
974
|
+
/**
|
|
975
|
+
* Create a new user.
|
|
976
|
+
*/
|
|
977
|
+
createUser = $action({
|
|
978
|
+
method: "POST",
|
|
979
|
+
path: this.url,
|
|
980
|
+
group: this.group,
|
|
981
|
+
description: "Create a new user",
|
|
982
|
+
schema: {
|
|
983
|
+
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
984
|
+
body: createUserSchema,
|
|
985
|
+
response: userResourceSchema
|
|
986
|
+
},
|
|
987
|
+
handler: ({ body, query }) => this.userService.createUser(body, query.userRealmName)
|
|
988
|
+
});
|
|
989
|
+
/**
|
|
990
|
+
* Update a user.
|
|
991
|
+
*/
|
|
992
|
+
updateUser = $action({
|
|
993
|
+
method: "PATCH",
|
|
994
|
+
path: `${this.url}/:id`,
|
|
995
|
+
group: this.group,
|
|
996
|
+
description: "Update a user",
|
|
997
|
+
schema: {
|
|
998
|
+
params: t.object({ id: t.uuid() }),
|
|
999
|
+
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1000
|
+
body: updateUserSchema,
|
|
1001
|
+
response: userResourceSchema
|
|
1002
|
+
},
|
|
1003
|
+
handler: ({ params, body, query }) => this.userService.updateUser(params.id, body, query.userRealmName)
|
|
1004
|
+
});
|
|
1005
|
+
/**
|
|
1006
|
+
* Delete a user.
|
|
1007
|
+
*/
|
|
1008
|
+
deleteUser = $action({
|
|
1009
|
+
method: "DELETE",
|
|
1010
|
+
path: `${this.url}/:id`,
|
|
1011
|
+
group: this.group,
|
|
1012
|
+
description: "Delete a user",
|
|
1013
|
+
schema: {
|
|
1014
|
+
params: t.object({ id: t.uuid() }),
|
|
1015
|
+
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1016
|
+
response: okSchema
|
|
1017
|
+
},
|
|
1018
|
+
handler: async ({ params, query }) => {
|
|
1019
|
+
await this.userService.deleteUser(params.id, query.userRealmName);
|
|
1020
|
+
return {
|
|
1021
|
+
ok: true,
|
|
1022
|
+
id: params.id
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
//#endregion
|
|
1029
|
+
//#region ../../src/api/users/schemas/completePasswordResetRequestSchema.ts
|
|
1030
|
+
/**
|
|
1031
|
+
* Request schema for completing a password reset.
|
|
1032
|
+
*
|
|
1033
|
+
* Requires the intent ID from Phase 1, the verification code,
|
|
1034
|
+
* and the new password.
|
|
1035
|
+
*/
|
|
1036
|
+
const completePasswordResetRequestSchema = t.object({
|
|
1037
|
+
intentId: t.uuid({ description: "The intent ID from createPasswordResetIntent" }),
|
|
1038
|
+
code: t.string({ description: "6-digit verification code sent via email" }),
|
|
1039
|
+
newPassword: t.string({
|
|
1040
|
+
minLength: 8,
|
|
1041
|
+
description: "New password (minimum 8 characters)"
|
|
1042
|
+
})
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
//#endregion
|
|
1046
|
+
//#region ../../src/api/users/schemas/completeRegistrationRequestSchema.ts
|
|
1047
|
+
const completeRegistrationRequestSchema = t.object({
|
|
1048
|
+
intentId: t.uuid({ description: "The registration intent ID from the first phase" }),
|
|
1049
|
+
emailCode: t.optional(t.string({ description: "Email verification code (if email verification required)" })),
|
|
1050
|
+
phoneCode: t.optional(t.string({ description: "Phone verification code (if phone verification required)" })),
|
|
1051
|
+
captchaToken: t.optional(t.string({ description: "Captcha token (if captcha required)" }))
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
//#endregion
|
|
1055
|
+
//#region ../../src/api/users/schemas/passwordResetIntentResponseSchema.ts
|
|
1056
|
+
/**
|
|
1057
|
+
* Response schema for password reset intent creation.
|
|
1058
|
+
*
|
|
1059
|
+
* Contains the intent ID needed for Phase 2 completion,
|
|
1060
|
+
* along with expiration time.
|
|
1061
|
+
*/
|
|
1062
|
+
const passwordResetIntentResponseSchema = t.object({
|
|
1063
|
+
intentId: t.uuid({ description: "Unique identifier for this password reset intent" }),
|
|
1064
|
+
expiresAt: t.datetime({ description: "ISO timestamp when this intent expires" })
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region ../../src/api/users/schemas/registerQuerySchema.ts
|
|
1069
|
+
/**
|
|
1070
|
+
* Schema for user registration query parameters.
|
|
1071
|
+
* Allows specifying a custom user realm.
|
|
1072
|
+
*/
|
|
1073
|
+
const registerQuerySchema = t.object({ userRealmName: t.optional(t.text({ description: "The user realm to register the user in (defaults to 'default')" })) });
|
|
1074
|
+
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region ../../src/api/users/schemas/registerRequestSchema.ts
|
|
1077
|
+
/**
|
|
1078
|
+
* Schema for user registration request body.
|
|
1079
|
+
* Password is always required, other fields depend on realm settings.
|
|
1080
|
+
*/
|
|
1081
|
+
const registerRequestSchema = t.object({
|
|
1082
|
+
password: t.string({
|
|
1083
|
+
minLength: 8,
|
|
1084
|
+
description: "Password for the account"
|
|
1085
|
+
}),
|
|
1086
|
+
username: t.optional(t.string({
|
|
1087
|
+
minLength: 3,
|
|
1088
|
+
description: "Unique username for the account"
|
|
1089
|
+
})),
|
|
1090
|
+
email: t.optional(t.string({
|
|
1091
|
+
format: "email",
|
|
1092
|
+
description: "User's email address"
|
|
1093
|
+
})),
|
|
1094
|
+
phoneNumber: t.optional(t.string({ description: "User's phone number" })),
|
|
1095
|
+
firstName: t.optional(t.string({ description: "User's first name" })),
|
|
1096
|
+
lastName: t.optional(t.string({ description: "User's last name" })),
|
|
1097
|
+
picture: t.optional(t.string({ description: "User's profile picture URL" }))
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
//#endregion
|
|
1101
|
+
//#region ../../src/api/users/schemas/registrationIntentResponseSchema.ts
|
|
1102
|
+
const registrationIntentResponseSchema = t.object({
|
|
1103
|
+
intentId: t.uuid({ description: "Unique identifier for the registration intent" }),
|
|
1104
|
+
expectCaptcha: t.boolean({ description: "Whether captcha verification is required" }),
|
|
1105
|
+
expectEmailVerification: t.boolean({ description: "Whether email verification is required" }),
|
|
1106
|
+
expectPhoneVerification: t.boolean({ description: "Whether phone verification is required" }),
|
|
1107
|
+
expiresAt: t.datetime({ description: "When the registration intent expires" })
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region ../../src/api/users/services/CredentialService.ts
|
|
1112
|
+
const INTENT_TTL_MINUTES$1 = 10;
|
|
1113
|
+
var CredentialService = class {
|
|
1114
|
+
log = $logger();
|
|
1115
|
+
cryptoProvider = $inject(CryptoProvider);
|
|
1116
|
+
dateTimeProvider = $inject(DateTimeProvider);
|
|
1117
|
+
verificationController = $client();
|
|
1118
|
+
userNotifications = $inject(UserNotifications);
|
|
1119
|
+
userRealmProvider = $inject(UserRealmProvider);
|
|
1120
|
+
auditService = $inject(AuditService);
|
|
1121
|
+
intentCache = $cache({
|
|
1122
|
+
name: "password-reset-intents",
|
|
1123
|
+
ttl: [INTENT_TTL_MINUTES$1, "minutes"]
|
|
1124
|
+
});
|
|
1125
|
+
users(userRealmName) {
|
|
1126
|
+
return this.userRealmProvider.userRepository(userRealmName);
|
|
1127
|
+
}
|
|
1128
|
+
sessions(userRealmName) {
|
|
1129
|
+
return this.userRealmProvider.sessionRepository(userRealmName);
|
|
1130
|
+
}
|
|
1131
|
+
identities(userRealmName) {
|
|
1132
|
+
return this.userRealmProvider.identityRepository(userRealmName);
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Phase 1: Create a password reset intent.
|
|
1136
|
+
*
|
|
1137
|
+
* Validates the email, checks for existing user with credentials,
|
|
1138
|
+
* sends verification code, and stores the intent in cache.
|
|
1139
|
+
*
|
|
1140
|
+
* @param email - User's email address
|
|
1141
|
+
* @param userRealmName - Optional realm name
|
|
1142
|
+
* @returns Intent response with intentId and expiration (always returns for security)
|
|
1143
|
+
*/
|
|
1144
|
+
async createPasswordResetIntent(email, userRealmName) {
|
|
1145
|
+
this.log.trace("Creating password reset intent", {
|
|
1146
|
+
email,
|
|
1147
|
+
userRealmName
|
|
1148
|
+
});
|
|
1149
|
+
const intentId = randomUUID();
|
|
1150
|
+
const expiresAt = this.dateTimeProvider.now().add(INTENT_TTL_MINUTES$1, "minutes").toISOString();
|
|
1151
|
+
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } }).catch(() => void 0);
|
|
1152
|
+
if (!user) {
|
|
1153
|
+
this.log.debug("Password reset requested for non-existent email", { email });
|
|
1154
|
+
return {
|
|
1155
|
+
intentId,
|
|
1156
|
+
expiresAt
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
const identity = await this.identities(userRealmName).findOne({ where: {
|
|
1160
|
+
userId: { eq: user.id },
|
|
1161
|
+
provider: { eq: "credentials" }
|
|
1162
|
+
} }).catch(() => void 0);
|
|
1163
|
+
if (!identity) {
|
|
1164
|
+
this.log.debug("Password reset requested for user without credentials", { userId: user.id });
|
|
1165
|
+
return {
|
|
1166
|
+
intentId,
|
|
1167
|
+
expiresAt
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
try {
|
|
1171
|
+
const verification = await this.verificationController.requestVerificationCode({
|
|
1172
|
+
params: { type: "code" },
|
|
1173
|
+
body: { target: email }
|
|
1174
|
+
});
|
|
1175
|
+
await this.userNotifications.passwordReset.push({
|
|
1176
|
+
contact: email,
|
|
1177
|
+
variables: {
|
|
1178
|
+
email,
|
|
1179
|
+
code: verification.token,
|
|
1180
|
+
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
const intent = {
|
|
1184
|
+
email,
|
|
1185
|
+
userId: user.id,
|
|
1186
|
+
identityId: identity.id,
|
|
1187
|
+
realmName: userRealmName,
|
|
1188
|
+
expiresAt
|
|
1189
|
+
};
|
|
1190
|
+
await this.intentCache.set(intentId, intent);
|
|
1191
|
+
this.log.info("Password reset intent created", {
|
|
1192
|
+
intentId,
|
|
1193
|
+
userId: user.id,
|
|
1194
|
+
email
|
|
1195
|
+
});
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
this.log.warn("Failed to create password reset verification", error);
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
intentId,
|
|
1201
|
+
expiresAt
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Phase 2: Complete password reset using an intent.
|
|
1206
|
+
*
|
|
1207
|
+
* Validates the verification code, updates the password,
|
|
1208
|
+
* and invalidates all existing sessions.
|
|
1209
|
+
*
|
|
1210
|
+
* @param body - Request body with intentId, code, and newPassword
|
|
1211
|
+
*/
|
|
1212
|
+
async completePasswordReset(body) {
|
|
1213
|
+
this.log.trace("Completing password reset", { intentId: body.intentId });
|
|
1214
|
+
const intent = await this.intentCache.get(body.intentId);
|
|
1215
|
+
if (!intent) {
|
|
1216
|
+
this.log.warn("Invalid or expired password reset intent", { intentId: body.intentId });
|
|
1217
|
+
throw new HttpError({
|
|
1218
|
+
status: 410,
|
|
1219
|
+
message: "Invalid or expired password reset intent"
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
if ((await this.verificationController.validateVerificationCode({
|
|
1223
|
+
params: { type: "code" },
|
|
1224
|
+
body: {
|
|
1225
|
+
target: intent.email,
|
|
1226
|
+
token: body.code
|
|
1227
|
+
}
|
|
1228
|
+
}).catch(() => {
|
|
1229
|
+
this.log.warn("Invalid verification code for password reset", {
|
|
1230
|
+
intentId: body.intentId,
|
|
1231
|
+
email: intent.email
|
|
1232
|
+
});
|
|
1233
|
+
throw new BadRequestError("Invalid or expired verification code");
|
|
1234
|
+
})).alreadyVerified) {
|
|
1235
|
+
this.log.warn("Verification code reuse attempt", {
|
|
1236
|
+
intentId: body.intentId,
|
|
1237
|
+
email: intent.email
|
|
1238
|
+
});
|
|
1239
|
+
throw new BadRequestError("Verification code has already been used");
|
|
1240
|
+
}
|
|
1241
|
+
await this.intentCache.invalidate(body.intentId);
|
|
871
1242
|
const hashedPassword = await this.cryptoProvider.hashPassword(body.newPassword);
|
|
872
1243
|
await this.identities(intent.realmName).updateById(intent.identityId, { password: hashedPassword });
|
|
873
1244
|
await this.sessions(intent.realmName).deleteMany({ userId: { eq: intent.userId } });
|
|
@@ -875,6 +1246,23 @@ var CredentialService = class {
|
|
|
875
1246
|
userId: intent.userId,
|
|
876
1247
|
email: intent.email
|
|
877
1248
|
});
|
|
1249
|
+
const realm = this.userRealmProvider.getRealm(intent.realmName);
|
|
1250
|
+
await this.auditService.recordUser("update", {
|
|
1251
|
+
userId: intent.userId,
|
|
1252
|
+
userEmail: intent.email,
|
|
1253
|
+
userRealm: realm.name,
|
|
1254
|
+
resourceId: intent.userId,
|
|
1255
|
+
description: "Password reset completed",
|
|
1256
|
+
metadata: { email: intent.email }
|
|
1257
|
+
});
|
|
1258
|
+
await this.auditService.record("security", "sessions_invalidated", {
|
|
1259
|
+
userId: intent.userId,
|
|
1260
|
+
userEmail: intent.email,
|
|
1261
|
+
userRealm: realm.name,
|
|
1262
|
+
resourceId: intent.userId,
|
|
1263
|
+
severity: "warning",
|
|
1264
|
+
description: "All sessions invalidated after password reset"
|
|
1265
|
+
});
|
|
878
1266
|
}
|
|
879
1267
|
/**
|
|
880
1268
|
* @deprecated Use createPasswordResetIntent instead
|
|
@@ -917,6 +1305,23 @@ var CredentialService = class {
|
|
|
917
1305
|
const hashedPassword = await this.cryptoProvider.hashPassword(newPassword);
|
|
918
1306
|
await this.identities(userRealmName).updateById(identity.id, { password: hashedPassword });
|
|
919
1307
|
await this.sessions(userRealmName).deleteMany({ userId: { eq: user.id } });
|
|
1308
|
+
const realm = this.userRealmProvider.getRealm(userRealmName);
|
|
1309
|
+
await this.auditService.recordUser("update", {
|
|
1310
|
+
userId: user.id,
|
|
1311
|
+
userEmail: email,
|
|
1312
|
+
userRealm: realm.name,
|
|
1313
|
+
resourceId: user.id,
|
|
1314
|
+
description: "Password reset completed (legacy)",
|
|
1315
|
+
metadata: { email }
|
|
1316
|
+
});
|
|
1317
|
+
await this.auditService.record("security", "sessions_invalidated", {
|
|
1318
|
+
userId: user.id,
|
|
1319
|
+
userEmail: email,
|
|
1320
|
+
userRealm: realm.name,
|
|
1321
|
+
resourceId: user.id,
|
|
1322
|
+
severity: "warning",
|
|
1323
|
+
description: "All sessions invalidated after password reset"
|
|
1324
|
+
});
|
|
920
1325
|
}
|
|
921
1326
|
};
|
|
922
1327
|
|
|
@@ -930,6 +1335,7 @@ var RegistrationService = class {
|
|
|
930
1335
|
verificationController = $client();
|
|
931
1336
|
userNotifications = $inject(UserNotifications);
|
|
932
1337
|
userRealmProvider = $inject(UserRealmProvider);
|
|
1338
|
+
auditService = $inject(AuditService);
|
|
933
1339
|
intentCache = $cache({
|
|
934
1340
|
name: "registration-intents",
|
|
935
1341
|
ttl: [INTENT_TTL_MINUTES, "minutes"]
|
|
@@ -1071,6 +1477,20 @@ var RegistrationService = class {
|
|
|
1071
1477
|
email: user.email,
|
|
1072
1478
|
username: user.username
|
|
1073
1479
|
});
|
|
1480
|
+
const realm = this.userRealmProvider.getRealm(userRealmName);
|
|
1481
|
+
await this.auditService.recordUser("create", {
|
|
1482
|
+
userId: user.id,
|
|
1483
|
+
userEmail: user.email ?? void 0,
|
|
1484
|
+
userRealm: realm.name,
|
|
1485
|
+
resourceId: user.id,
|
|
1486
|
+
description: "User registered",
|
|
1487
|
+
metadata: {
|
|
1488
|
+
username: user.username,
|
|
1489
|
+
email: user.email,
|
|
1490
|
+
emailVerified: user.emailVerified,
|
|
1491
|
+
registrationMethod: "credentials"
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1074
1494
|
return user;
|
|
1075
1495
|
}
|
|
1076
1496
|
/**
|
|
@@ -1092,317 +1512,95 @@ var RegistrationService = class {
|
|
|
1092
1512
|
}
|
|
1093
1513
|
if (body.phoneNumber) {
|
|
1094
1514
|
if (await userRepository.findOne({ where: { phoneNumber: { eq: body.phoneNumber } } }).catch(() => void 0)) {
|
|
1095
|
-
this.log.debug("Phone number already taken", { phoneNumber: body.phoneNumber });
|
|
1096
|
-
throw new ConflictError("User with this phone number already exists");
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Send email verification code.
|
|
1102
|
-
*/
|
|
1103
|
-
async sendEmailVerification(email) {
|
|
1104
|
-
this.log.debug("Sending email verification code", { email });
|
|
1105
|
-
try {
|
|
1106
|
-
const verification = await this.verificationController.requestVerificationCode({
|
|
1107
|
-
params: { type: "code" },
|
|
1108
|
-
body: { target: email }
|
|
1109
|
-
});
|
|
1110
|
-
await this.userNotifications.emailVerification.push({
|
|
1111
|
-
contact: email,
|
|
1112
|
-
variables: {
|
|
1113
|
-
email,
|
|
1114
|
-
code: verification.token,
|
|
1115
|
-
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
this.log.debug("Email verification code sent", { email });
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
this.log.warn("Failed to send email verification code", error);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
/**
|
|
1124
|
-
* Send phone verification code.
|
|
1125
|
-
*/
|
|
1126
|
-
async sendPhoneVerification(phoneNumber) {
|
|
1127
|
-
this.log.debug("Sending phone verification code", { phoneNumber });
|
|
1128
|
-
try {
|
|
1129
|
-
const verification = await this.verificationController.requestVerificationCode({
|
|
1130
|
-
params: { type: "code" },
|
|
1131
|
-
body: { target: phoneNumber }
|
|
1132
|
-
});
|
|
1133
|
-
await this.userNotifications.phoneVerification.push({
|
|
1134
|
-
contact: phoneNumber,
|
|
1135
|
-
variables: {
|
|
1136
|
-
phoneNumber,
|
|
1137
|
-
code: verification.token,
|
|
1138
|
-
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1139
|
-
}
|
|
1140
|
-
});
|
|
1141
|
-
this.log.debug("Phone verification code sent", { phoneNumber });
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
this.log.warn("Failed to send phone verification code", {
|
|
1144
|
-
phoneNumber,
|
|
1145
|
-
error
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Verify email code using verification service.
|
|
1151
|
-
*/
|
|
1152
|
-
async verifyEmailCode(email, code) {
|
|
1153
|
-
if ((await this.verificationController.validateVerificationCode({
|
|
1154
|
-
params: { type: "code" },
|
|
1155
|
-
body: {
|
|
1156
|
-
target: email,
|
|
1157
|
-
token: code
|
|
1158
|
-
}
|
|
1159
|
-
}).catch(() => {
|
|
1160
|
-
this.log.warn("Invalid email verification code", { email });
|
|
1161
|
-
throw new BadRequestError("Invalid or expired email verification code");
|
|
1162
|
-
})).alreadyVerified) {
|
|
1163
|
-
this.log.warn("Email verification code already used", { email });
|
|
1164
|
-
throw new BadRequestError("Email verification code has already been used");
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
/**
|
|
1168
|
-
* Verify phone code using verification service.
|
|
1169
|
-
*/
|
|
1170
|
-
async verifyPhoneCode(phoneNumber, code) {
|
|
1171
|
-
if ((await this.verificationController.validateVerificationCode({
|
|
1172
|
-
params: { type: "code" },
|
|
1173
|
-
body: {
|
|
1174
|
-
target: phoneNumber,
|
|
1175
|
-
token: code
|
|
1176
|
-
}
|
|
1177
|
-
}).catch(() => {
|
|
1178
|
-
this.log.warn("Invalid phone verification code", { phoneNumber });
|
|
1179
|
-
throw new BadRequestError("Invalid or expired phone verification code");
|
|
1180
|
-
})).alreadyVerified) {
|
|
1181
|
-
this.log.warn("Phone verification code already used", { phoneNumber });
|
|
1182
|
-
throw new BadRequestError("Phone verification code has already been used");
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
};
|
|
1186
|
-
|
|
1187
|
-
//#endregion
|
|
1188
|
-
//#region ../../src/api/users/services/UserService.ts
|
|
1189
|
-
var UserService = class {
|
|
1190
|
-
log = $logger();
|
|
1191
|
-
verificationController = $client();
|
|
1192
|
-
userNotifications = $inject(UserNotifications);
|
|
1193
|
-
userRealmProvider = $inject(UserRealmProvider);
|
|
1194
|
-
users(userRealmName) {
|
|
1195
|
-
return this.userRealmProvider.userRepository(userRealmName);
|
|
1196
|
-
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Request email verification for a user.
|
|
1199
|
-
* @param email - The email address to verify.
|
|
1200
|
-
* @param userRealmName - Optional realm name.
|
|
1201
|
-
* @param method - The verification method: "code" (default) or "link".
|
|
1202
|
-
* @param verifyUrl - Base URL for verification link (required when method is "link").
|
|
1203
|
-
*/
|
|
1204
|
-
async requestEmailVerification(email, userRealmName, method = "code", verifyUrl) {
|
|
1205
|
-
this.log.trace("Requesting email verification", {
|
|
1206
|
-
email,
|
|
1207
|
-
userRealmName,
|
|
1208
|
-
method
|
|
1209
|
-
});
|
|
1210
|
-
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } }).catch(() => void 0);
|
|
1211
|
-
if (!user) {
|
|
1212
|
-
this.log.debug("Email verification requested for non-existent user", { email });
|
|
1213
|
-
return true;
|
|
1214
|
-
}
|
|
1215
|
-
if (user.emailVerified) {
|
|
1216
|
-
this.log.debug("Email verification requested for already verified user", {
|
|
1217
|
-
email,
|
|
1218
|
-
userId: user.id
|
|
1219
|
-
});
|
|
1220
|
-
return true;
|
|
1221
|
-
}
|
|
1222
|
-
try {
|
|
1223
|
-
const verification = await this.verificationController.requestVerificationCode({
|
|
1224
|
-
params: { type: method },
|
|
1225
|
-
body: { target: email }
|
|
1226
|
-
});
|
|
1227
|
-
if (method === "link") {
|
|
1228
|
-
const url = new URL(verifyUrl || "/verify-email", "http://localhost");
|
|
1229
|
-
url.searchParams.set("email", email);
|
|
1230
|
-
url.searchParams.set("token", verification.token);
|
|
1231
|
-
const fullVerifyUrl = verifyUrl ? `${verifyUrl}${url.search}` : url.pathname + url.search;
|
|
1232
|
-
await this.userNotifications.emailVerificationLink.push({
|
|
1233
|
-
contact: email,
|
|
1234
|
-
variables: {
|
|
1235
|
-
email,
|
|
1236
|
-
verifyUrl: fullVerifyUrl,
|
|
1237
|
-
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1238
|
-
}
|
|
1239
|
-
});
|
|
1240
|
-
this.log.debug("Email verification link sent", {
|
|
1241
|
-
email,
|
|
1242
|
-
userId: user.id
|
|
1243
|
-
});
|
|
1244
|
-
} else {
|
|
1245
|
-
await this.userNotifications.emailVerification.push({
|
|
1246
|
-
contact: email,
|
|
1247
|
-
variables: {
|
|
1248
|
-
email,
|
|
1249
|
-
code: verification.token,
|
|
1250
|
-
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
this.log.debug("Email verification code sent", {
|
|
1254
|
-
email,
|
|
1255
|
-
userId: user.id
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
} catch (error) {
|
|
1259
|
-
this.log.warn("Failed to send email verification", {
|
|
1260
|
-
email,
|
|
1261
|
-
error
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
|
-
return true;
|
|
1265
|
-
}
|
|
1266
|
-
/**
|
|
1267
|
-
* Verify a user's email using a valid verification token.
|
|
1268
|
-
* Supports both code (6-digit) and link (UUID) verification tokens.
|
|
1269
|
-
*/
|
|
1270
|
-
async verifyEmail(email, token, userRealmName) {
|
|
1271
|
-
this.log.trace("Verifying email", {
|
|
1272
|
-
email,
|
|
1273
|
-
userRealmName
|
|
1274
|
-
});
|
|
1275
|
-
const type = /^\d{6}$/.test(token) ? "code" : "link";
|
|
1276
|
-
if ((await this.verificationController.validateVerificationCode({
|
|
1277
|
-
params: { type },
|
|
1278
|
-
body: {
|
|
1279
|
-
target: email,
|
|
1280
|
-
token
|
|
1281
|
-
}
|
|
1282
|
-
}).catch(() => {
|
|
1283
|
-
this.log.warn("Invalid email verification token", {
|
|
1284
|
-
email,
|
|
1285
|
-
type
|
|
1286
|
-
});
|
|
1287
|
-
throw new BadRequestError("Invalid or expired verification token");
|
|
1288
|
-
})).alreadyVerified) {
|
|
1289
|
-
this.log.warn("Email verification token already used", { email });
|
|
1290
|
-
throw new BadRequestError("Invalid or expired verification token");
|
|
1291
|
-
}
|
|
1292
|
-
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } });
|
|
1293
|
-
await this.users(userRealmName).updateById(user.id, { emailVerified: true });
|
|
1294
|
-
this.log.info("Email verified", {
|
|
1295
|
-
email,
|
|
1296
|
-
userId: user.id,
|
|
1297
|
-
type
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Check if an email is verified.
|
|
1302
|
-
*/
|
|
1303
|
-
async isEmailVerified(email, userRealmName) {
|
|
1304
|
-
this.log.trace("Checking if email is verified", {
|
|
1305
|
-
email,
|
|
1306
|
-
userRealmName
|
|
1307
|
-
});
|
|
1308
|
-
return (await this.users(userRealmName).findOne({ where: { email: { eq: email } } }).catch(() => void 0))?.emailVerified ?? false;
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Find users with pagination and filtering.
|
|
1312
|
-
*/
|
|
1313
|
-
async findUsers(q = {}, userRealmName) {
|
|
1314
|
-
this.log.trace("Finding users", {
|
|
1315
|
-
query: q,
|
|
1316
|
-
userRealmName
|
|
1317
|
-
});
|
|
1318
|
-
q.sort ??= "-createdAt";
|
|
1319
|
-
const where = this.users(userRealmName).createQueryWhere();
|
|
1320
|
-
if (q.email) where.email = { like: q.email };
|
|
1321
|
-
if (q.enabled !== void 0) where.enabled = { eq: q.enabled };
|
|
1322
|
-
if (q.emailVerified !== void 0) where.emailVerified = { eq: q.emailVerified };
|
|
1323
|
-
if (q.roles) where.roles = { arrayContains: q.roles };
|
|
1324
|
-
if (q.query) Object.assign(where, parseQueryString(q.query));
|
|
1325
|
-
const result = await this.users(userRealmName).paginate(q, { where }, { count: true });
|
|
1326
|
-
this.log.debug("Users found", {
|
|
1327
|
-
count: result.content.length,
|
|
1328
|
-
total: result.page.totalElements
|
|
1329
|
-
});
|
|
1330
|
-
return result;
|
|
1515
|
+
this.log.debug("Phone number already taken", { phoneNumber: body.phoneNumber });
|
|
1516
|
+
throw new ConflictError("User with this phone number already exists");
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1331
1519
|
}
|
|
1332
1520
|
/**
|
|
1333
|
-
*
|
|
1521
|
+
* Send email verification code.
|
|
1334
1522
|
*/
|
|
1335
|
-
async
|
|
1336
|
-
this.log.
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1523
|
+
async sendEmailVerification(email) {
|
|
1524
|
+
this.log.debug("Sending email verification code", { email });
|
|
1525
|
+
try {
|
|
1526
|
+
const verification = await this.verificationController.requestVerificationCode({
|
|
1527
|
+
params: { type: "code" },
|
|
1528
|
+
body: { target: email }
|
|
1529
|
+
});
|
|
1530
|
+
await this.userNotifications.emailVerification.push({
|
|
1531
|
+
contact: email,
|
|
1532
|
+
variables: {
|
|
1533
|
+
email,
|
|
1534
|
+
code: verification.token,
|
|
1535
|
+
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
this.log.debug("Email verification code sent", { email });
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
this.log.warn("Failed to send email verification code", error);
|
|
1541
|
+
}
|
|
1341
1542
|
}
|
|
1342
1543
|
/**
|
|
1343
|
-
*
|
|
1544
|
+
* Send phone verification code.
|
|
1344
1545
|
*/
|
|
1345
|
-
async
|
|
1346
|
-
this.log.
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
this.log.debug("Phone number already taken", { phoneNumber: data.phoneNumber });
|
|
1367
|
-
throw new BadRequestError("User with this phone number already exists");
|
|
1368
|
-
}
|
|
1546
|
+
async sendPhoneVerification(phoneNumber) {
|
|
1547
|
+
this.log.debug("Sending phone verification code", { phoneNumber });
|
|
1548
|
+
try {
|
|
1549
|
+
const verification = await this.verificationController.requestVerificationCode({
|
|
1550
|
+
params: { type: "code" },
|
|
1551
|
+
body: { target: phoneNumber }
|
|
1552
|
+
});
|
|
1553
|
+
await this.userNotifications.phoneVerification.push({
|
|
1554
|
+
contact: phoneNumber,
|
|
1555
|
+
variables: {
|
|
1556
|
+
phoneNumber,
|
|
1557
|
+
code: verification.token,
|
|
1558
|
+
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
this.log.debug("Phone verification code sent", { phoneNumber });
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
this.log.warn("Failed to send phone verification code", {
|
|
1564
|
+
phoneNumber,
|
|
1565
|
+
error
|
|
1566
|
+
});
|
|
1369
1567
|
}
|
|
1370
|
-
const user = await this.users(userRealmName).create({
|
|
1371
|
-
...data,
|
|
1372
|
-
roles: data.roles ?? ["user"],
|
|
1373
|
-
realm: realm.name
|
|
1374
|
-
});
|
|
1375
|
-
this.log.info("User created", {
|
|
1376
|
-
userId: user.id,
|
|
1377
|
-
username: user.username,
|
|
1378
|
-
email: user.email
|
|
1379
|
-
});
|
|
1380
|
-
return user;
|
|
1381
1568
|
}
|
|
1382
1569
|
/**
|
|
1383
|
-
*
|
|
1570
|
+
* Verify email code using verification service.
|
|
1384
1571
|
*/
|
|
1385
|
-
async
|
|
1386
|
-
this.
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1572
|
+
async verifyEmailCode(email, code) {
|
|
1573
|
+
if ((await this.verificationController.validateVerificationCode({
|
|
1574
|
+
params: { type: "code" },
|
|
1575
|
+
body: {
|
|
1576
|
+
target: email,
|
|
1577
|
+
token: code
|
|
1578
|
+
}
|
|
1579
|
+
}).catch(() => {
|
|
1580
|
+
this.log.warn("Invalid email verification code", { email });
|
|
1581
|
+
throw new BadRequestError("Invalid or expired email verification code");
|
|
1582
|
+
})).alreadyVerified) {
|
|
1583
|
+
this.log.warn("Email verification code already used", { email });
|
|
1584
|
+
throw new BadRequestError("Email verification code has already been used");
|
|
1585
|
+
}
|
|
1394
1586
|
}
|
|
1395
1587
|
/**
|
|
1396
|
-
*
|
|
1588
|
+
* Verify phone code using verification service.
|
|
1397
1589
|
*/
|
|
1398
|
-
async
|
|
1399
|
-
this.
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1590
|
+
async verifyPhoneCode(phoneNumber, code) {
|
|
1591
|
+
if ((await this.verificationController.validateVerificationCode({
|
|
1592
|
+
params: { type: "code" },
|
|
1593
|
+
body: {
|
|
1594
|
+
target: phoneNumber,
|
|
1595
|
+
token: code
|
|
1596
|
+
}
|
|
1597
|
+
}).catch(() => {
|
|
1598
|
+
this.log.warn("Invalid phone verification code", { phoneNumber });
|
|
1599
|
+
throw new BadRequestError("Invalid or expired phone verification code");
|
|
1600
|
+
})).alreadyVerified) {
|
|
1601
|
+
this.log.warn("Phone verification code already used", { phoneNumber });
|
|
1602
|
+
throw new BadRequestError("Phone verification code has already been used");
|
|
1603
|
+
}
|
|
1406
1604
|
}
|
|
1407
1605
|
};
|
|
1408
1606
|
|
|
@@ -1419,6 +1617,7 @@ var UserController = class {
|
|
|
1419
1617
|
* Validates data, creates verification sessions, and stores intent in cache.
|
|
1420
1618
|
*/
|
|
1421
1619
|
createRegistrationIntent = $action({
|
|
1620
|
+
group: this.group,
|
|
1422
1621
|
method: "POST",
|
|
1423
1622
|
path: `${this.url}/register`,
|
|
1424
1623
|
secure: false,
|
|
@@ -1430,55 +1629,11 @@ var UserController = class {
|
|
|
1430
1629
|
handler: ({ body, query }) => this.registrationService.createRegistrationIntent(body, query.userRealmName)
|
|
1431
1630
|
});
|
|
1432
1631
|
/**
|
|
1433
|
-
* Find users with pagination and filtering.
|
|
1434
|
-
*/
|
|
1435
|
-
findUsers = $action({
|
|
1436
|
-
path: this.url,
|
|
1437
|
-
group: this.group,
|
|
1438
|
-
description: "Find users with pagination and filtering",
|
|
1439
|
-
schema: {
|
|
1440
|
-
query: t.extend(userQuerySchema, { userRealmName: t.optional(t.string()) }),
|
|
1441
|
-
response: pg.page(userResourceSchema)
|
|
1442
|
-
},
|
|
1443
|
-
handler: ({ query }) => {
|
|
1444
|
-
const { userRealmName, ...q } = query;
|
|
1445
|
-
return this.userService.findUsers(q, userRealmName);
|
|
1446
|
-
}
|
|
1447
|
-
});
|
|
1448
|
-
/**
|
|
1449
|
-
* Get a user by ID.
|
|
1450
|
-
*/
|
|
1451
|
-
getUser = $action({
|
|
1452
|
-
path: `${this.url}/:id`,
|
|
1453
|
-
group: this.group,
|
|
1454
|
-
description: "Get a user by ID",
|
|
1455
|
-
schema: {
|
|
1456
|
-
params: t.object({ id: t.uuid() }),
|
|
1457
|
-
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1458
|
-
response: userResourceSchema
|
|
1459
|
-
},
|
|
1460
|
-
handler: ({ params, query }) => this.userService.getUserById(params.id, query.userRealmName)
|
|
1461
|
-
});
|
|
1462
|
-
/**
|
|
1463
|
-
* Create a new user.
|
|
1464
|
-
*/
|
|
1465
|
-
createUser = $action({
|
|
1466
|
-
method: "POST",
|
|
1467
|
-
path: this.url,
|
|
1468
|
-
group: this.group,
|
|
1469
|
-
description: "Create a new user",
|
|
1470
|
-
schema: {
|
|
1471
|
-
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1472
|
-
body: createUserSchema,
|
|
1473
|
-
response: userResourceSchema
|
|
1474
|
-
},
|
|
1475
|
-
handler: ({ body, query }) => this.userService.createUser(body, query.userRealmName)
|
|
1476
|
-
});
|
|
1477
|
-
/**
|
|
1478
1632
|
* Phase 2: Complete registration using an intent.
|
|
1479
1633
|
* Validates verification codes and creates the user.
|
|
1480
1634
|
*/
|
|
1481
1635
|
createUserFromIntent = $action({
|
|
1636
|
+
group: this.group,
|
|
1482
1637
|
method: "POST",
|
|
1483
1638
|
path: `${this.url}/register/complete`,
|
|
1484
1639
|
secure: false,
|
|
@@ -1489,47 +1644,11 @@ var UserController = class {
|
|
|
1489
1644
|
handler: ({ body }) => this.registrationService.completeRegistration(body)
|
|
1490
1645
|
});
|
|
1491
1646
|
/**
|
|
1492
|
-
* Update a user.
|
|
1493
|
-
*/
|
|
1494
|
-
updateUser = $action({
|
|
1495
|
-
method: "PATCH",
|
|
1496
|
-
path: `${this.url}/:id`,
|
|
1497
|
-
group: this.group,
|
|
1498
|
-
description: "Update a user",
|
|
1499
|
-
schema: {
|
|
1500
|
-
params: t.object({ id: t.uuid() }),
|
|
1501
|
-
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1502
|
-
body: updateUserSchema,
|
|
1503
|
-
response: userResourceSchema
|
|
1504
|
-
},
|
|
1505
|
-
handler: ({ params, body, query }) => this.userService.updateUser(params.id, body, query.userRealmName)
|
|
1506
|
-
});
|
|
1507
|
-
/**
|
|
1508
|
-
* Delete a user.
|
|
1509
|
-
*/
|
|
1510
|
-
deleteUser = $action({
|
|
1511
|
-
method: "DELETE",
|
|
1512
|
-
path: `${this.url}/:id`,
|
|
1513
|
-
group: this.group,
|
|
1514
|
-
description: "Delete a user",
|
|
1515
|
-
schema: {
|
|
1516
|
-
params: t.object({ id: t.uuid() }),
|
|
1517
|
-
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1518
|
-
response: okSchema
|
|
1519
|
-
},
|
|
1520
|
-
handler: async ({ params, query }) => {
|
|
1521
|
-
await this.userService.deleteUser(params.id, query.userRealmName);
|
|
1522
|
-
return {
|
|
1523
|
-
ok: true,
|
|
1524
|
-
id: params.id
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
});
|
|
1528
|
-
/**
|
|
1529
1647
|
* Phase 1: Create a password reset intent.
|
|
1530
1648
|
* Validates email, sends verification code, and stores intent in cache.
|
|
1531
1649
|
*/
|
|
1532
1650
|
createPasswordResetIntent = $action({
|
|
1651
|
+
group: this.group,
|
|
1533
1652
|
method: "POST",
|
|
1534
1653
|
path: `${this.url}/password-reset`,
|
|
1535
1654
|
secure: false,
|
|
@@ -1545,6 +1664,7 @@ var UserController = class {
|
|
|
1545
1664
|
* Validates verification code, updates password, and invalidates sessions.
|
|
1546
1665
|
*/
|
|
1547
1666
|
completePasswordReset = $action({
|
|
1667
|
+
group: this.group,
|
|
1548
1668
|
method: "POST",
|
|
1549
1669
|
path: `${this.url}/password-reset/complete`,
|
|
1550
1670
|
secure: false,
|
|
@@ -1727,6 +1847,7 @@ const userRealmConfigSchema = t.object({
|
|
|
1727
1847
|
*/
|
|
1728
1848
|
var UserRealmController = class {
|
|
1729
1849
|
url = "/realms";
|
|
1850
|
+
group = "realms";
|
|
1730
1851
|
userRealmProvider = $inject(UserRealmProvider);
|
|
1731
1852
|
serverAuthProvider = $inject(ServerAuthProvider);
|
|
1732
1853
|
/**
|
|
@@ -1734,6 +1855,7 @@ var UserRealmController = class {
|
|
|
1734
1855
|
* This endpoint is not exposed in the API documentation.
|
|
1735
1856
|
*/
|
|
1736
1857
|
getRealmConfig = $action({
|
|
1858
|
+
group: this.group,
|
|
1737
1859
|
method: "GET",
|
|
1738
1860
|
path: `${this.url}/config`,
|
|
1739
1861
|
secure: false,
|
|
@@ -1755,6 +1877,7 @@ var UserRealmController = class {
|
|
|
1755
1877
|
}
|
|
1756
1878
|
});
|
|
1757
1879
|
checkUsernameAvailability = $action({
|
|
1880
|
+
group: this.group,
|
|
1758
1881
|
path: `${this.url}/check-username`,
|
|
1759
1882
|
secure: false,
|
|
1760
1883
|
schema: {
|
|
@@ -1779,6 +1902,7 @@ var SessionService = class {
|
|
|
1779
1902
|
log = $logger();
|
|
1780
1903
|
userRealmProvider = $inject(UserRealmProvider);
|
|
1781
1904
|
fileController = $client();
|
|
1905
|
+
auditService = $inject(AuditService);
|
|
1782
1906
|
users(userRealmName) {
|
|
1783
1907
|
return this.userRealmProvider.userRepository(userRealmName);
|
|
1784
1908
|
}
|
|
@@ -1818,6 +1942,14 @@ var SessionService = class {
|
|
|
1818
1942
|
username,
|
|
1819
1943
|
realm: name
|
|
1820
1944
|
});
|
|
1945
|
+
await this.auditService.recordAuth("login_failed", {
|
|
1946
|
+
userRealm: name,
|
|
1947
|
+
description: "Invalid login identifier format",
|
|
1948
|
+
metadata: {
|
|
1949
|
+
provider,
|
|
1950
|
+
username
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1821
1953
|
throw new InvalidCredentialsError();
|
|
1822
1954
|
}
|
|
1823
1955
|
const user = await users$1.findOne({ where }).catch(() => void 0);
|
|
@@ -1827,6 +1959,14 @@ var SessionService = class {
|
|
|
1827
1959
|
username,
|
|
1828
1960
|
realm: name
|
|
1829
1961
|
});
|
|
1962
|
+
await this.auditService.recordAuth("login_failed", {
|
|
1963
|
+
userRealm: name,
|
|
1964
|
+
description: "User not found",
|
|
1965
|
+
metadata: {
|
|
1966
|
+
provider,
|
|
1967
|
+
username
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1830
1970
|
throw new InvalidCredentialsError();
|
|
1831
1971
|
}
|
|
1832
1972
|
const identity = await identities$1.findOne({ where: {
|
|
@@ -1849,8 +1989,28 @@ var SessionService = class {
|
|
|
1849
1989
|
username,
|
|
1850
1990
|
realm: name
|
|
1851
1991
|
});
|
|
1992
|
+
await this.auditService.recordAuth("login_failed", {
|
|
1993
|
+
userRealm: name,
|
|
1994
|
+
resourceId: user.id,
|
|
1995
|
+
description: "Invalid password",
|
|
1996
|
+
metadata: {
|
|
1997
|
+
provider,
|
|
1998
|
+
username
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
1852
2001
|
throw new InvalidCredentialsError();
|
|
1853
2002
|
}
|
|
2003
|
+
await this.auditService.recordAuth("login", {
|
|
2004
|
+
userId: user.id,
|
|
2005
|
+
userEmail: user.email ?? void 0,
|
|
2006
|
+
userRealm: name,
|
|
2007
|
+
resourceId: user.id,
|
|
2008
|
+
description: `User logged in via ${provider}`,
|
|
2009
|
+
metadata: {
|
|
2010
|
+
provider,
|
|
2011
|
+
username
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
1854
2014
|
return user;
|
|
1855
2015
|
} catch (error) {
|
|
1856
2016
|
if (error instanceof InvalidCredentialsError) throw error;
|
|
@@ -1901,6 +2061,14 @@ var SessionService = class {
|
|
|
1901
2061
|
sessionId: session.id,
|
|
1902
2062
|
userId: session.userId
|
|
1903
2063
|
});
|
|
2064
|
+
const { name } = this.userRealmProvider.getRealm(userRealmName);
|
|
2065
|
+
await this.auditService.recordAuth("token_refresh", {
|
|
2066
|
+
userId: user.id,
|
|
2067
|
+
userEmail: user.email ?? void 0,
|
|
2068
|
+
userRealm: name,
|
|
2069
|
+
sessionId: session.id,
|
|
2070
|
+
description: "Session token refreshed"
|
|
2071
|
+
});
|
|
1904
2072
|
return {
|
|
1905
2073
|
user,
|
|
1906
2074
|
expiresIn: expiresAt.unix() - now.unix(),
|
|
@@ -1909,8 +2077,18 @@ var SessionService = class {
|
|
|
1909
2077
|
}
|
|
1910
2078
|
async deleteSession(refreshToken, userRealmName) {
|
|
1911
2079
|
this.log.trace("Deleting session");
|
|
2080
|
+
const session = await this.sessions(userRealmName).findOne({ where: { refreshToken: { eq: refreshToken } } }).catch(() => void 0);
|
|
1912
2081
|
await this.sessions(userRealmName).deleteOne({ refreshToken });
|
|
1913
2082
|
this.log.debug("Session deleted");
|
|
2083
|
+
if (session) {
|
|
2084
|
+
const { name } = this.userRealmProvider.getRealm(userRealmName);
|
|
2085
|
+
await this.auditService.recordAuth("logout", {
|
|
2086
|
+
userId: session.userId,
|
|
2087
|
+
userRealm: name,
|
|
2088
|
+
sessionId: session.id,
|
|
2089
|
+
description: "User logged out"
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
1914
2092
|
}
|
|
1915
2093
|
async link(provider, profile, userRealmName) {
|
|
1916
2094
|
this.log.trace("Linking OAuth2 profile", {
|
|
@@ -1931,7 +2109,19 @@ var SessionService = class {
|
|
|
1931
2109
|
identityId: identity.id,
|
|
1932
2110
|
userId: identity.userId
|
|
1933
2111
|
});
|
|
1934
|
-
|
|
2112
|
+
const user$1 = await users$1.findById(identity.userId);
|
|
2113
|
+
await this.auditService.recordAuth("login", {
|
|
2114
|
+
userId: user$1.id,
|
|
2115
|
+
userEmail: user$1.email ?? void 0,
|
|
2116
|
+
userRealm: realm.name,
|
|
2117
|
+
resourceId: user$1.id,
|
|
2118
|
+
description: `User logged in via OAuth2 (${provider})`,
|
|
2119
|
+
metadata: {
|
|
2120
|
+
provider,
|
|
2121
|
+
providerUserId: profile.sub
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
return user$1;
|
|
1935
2125
|
}
|
|
1936
2126
|
if (!profile.email) {
|
|
1937
2127
|
this.log.debug("OAuth2 profile has no email, returning profile as-is", {
|
|
@@ -1956,6 +2146,18 @@ var SessionService = class {
|
|
|
1956
2146
|
providerUserId: profile.sub,
|
|
1957
2147
|
userId: existing.id
|
|
1958
2148
|
});
|
|
2149
|
+
await this.auditService.recordAuth("login", {
|
|
2150
|
+
userId: existing.id,
|
|
2151
|
+
userEmail: existing.email ?? void 0,
|
|
2152
|
+
userRealm: realm.name,
|
|
2153
|
+
resourceId: existing.id,
|
|
2154
|
+
description: `OAuth2 identity linked to existing user (${provider})`,
|
|
2155
|
+
metadata: {
|
|
2156
|
+
provider,
|
|
2157
|
+
providerUserId: profile.sub,
|
|
2158
|
+
linked: true
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
1959
2161
|
return existing;
|
|
1960
2162
|
}
|
|
1961
2163
|
const user = await users$1.create({
|
|
@@ -1992,6 +2194,31 @@ var SessionService = class {
|
|
|
1992
2194
|
email: user.email,
|
|
1993
2195
|
username: user.username
|
|
1994
2196
|
});
|
|
2197
|
+
await this.auditService.recordUser("create", {
|
|
2198
|
+
userId: user.id,
|
|
2199
|
+
userEmail: user.email ?? void 0,
|
|
2200
|
+
userRealm: realm.name,
|
|
2201
|
+
resourceId: user.id,
|
|
2202
|
+
description: `User created via OAuth2 (${provider})`,
|
|
2203
|
+
metadata: {
|
|
2204
|
+
provider,
|
|
2205
|
+
providerUserId: profile.sub,
|
|
2206
|
+
username: user.username,
|
|
2207
|
+
email: user.email
|
|
2208
|
+
}
|
|
2209
|
+
});
|
|
2210
|
+
await this.auditService.recordAuth("login", {
|
|
2211
|
+
userId: user.id,
|
|
2212
|
+
userEmail: user.email ?? void 0,
|
|
2213
|
+
userRealm: realm.name,
|
|
2214
|
+
resourceId: user.id,
|
|
2215
|
+
description: `First login via OAuth2 (${provider})`,
|
|
2216
|
+
metadata: {
|
|
2217
|
+
provider,
|
|
2218
|
+
providerUserId: profile.sub,
|
|
2219
|
+
firstLogin: true
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
1995
2222
|
return user;
|
|
1996
2223
|
}
|
|
1997
2224
|
};
|
|
@@ -2017,10 +2244,13 @@ const $userRealm = (options = {}) => {
|
|
|
2017
2244
|
const securityProvider = alepha.inject(SecurityProvider);
|
|
2018
2245
|
const userRealmProvider = alepha.inject(UserRealmProvider);
|
|
2019
2246
|
const name = options.realm?.name ?? DEFAULT_USER_REALM_NAME;
|
|
2247
|
+
options.settings ??= {};
|
|
2248
|
+
if (options.settings.emailRequired) options.settings.emailEnabled = true;
|
|
2249
|
+
if (options.settings.usernameRequired) options.settings.usernameEnabled = true;
|
|
2250
|
+
if (options.settings.phoneRequired) options.settings.phoneEnabled = true;
|
|
2020
2251
|
const userRealm = userRealmProvider.register(name, options);
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
if (options.modules?.jobs) alepha.with(AlephaApiJobs);
|
|
2252
|
+
alepha.with(AlephaApiFiles);
|
|
2253
|
+
alepha.with(AlephaApiAudits);
|
|
2024
2254
|
const realm = $realm({
|
|
2025
2255
|
...options.realm,
|
|
2026
2256
|
name,
|
|
@@ -2154,13 +2384,14 @@ const AlephaApiUsers = $module({
|
|
|
2154
2384
|
UserService,
|
|
2155
2385
|
IdentityService,
|
|
2156
2386
|
UserController,
|
|
2157
|
-
|
|
2158
|
-
|
|
2387
|
+
AdminUserController,
|
|
2388
|
+
AdminSessionController,
|
|
2389
|
+
AdminIdentityController,
|
|
2159
2390
|
UserRealmController,
|
|
2160
2391
|
UserNotifications
|
|
2161
2392
|
]
|
|
2162
2393
|
});
|
|
2163
2394
|
|
|
2164
2395
|
//#endregion
|
|
2165
|
-
export { $userRealm, AlephaApiUsers, CredentialService, DEFAULT_USER_REALM_NAME,
|
|
2396
|
+
export { $userRealm, AdminIdentityController, AdminSessionController, AdminUserController, AlephaApiUsers, CredentialService, DEFAULT_USER_REALM_NAME, IdentityService, RegistrationService, SessionCrudService, SessionService, UserController, UserRealmController, UserRealmProvider, UserService, completePasswordResetRequestSchema, completeRegistrationRequestSchema, createUserSchema, identities, identityQuerySchema, identityResourceSchema, loginSchema, passwordResetIntentResponseSchema, realmAuthSettingsAtom, registerSchema, registrationIntentResponseSchema, resetPasswordRequestSchema, resetPasswordSchema, sessionQuerySchema, sessionResourceSchema, sessions, updateUserSchema, userQuerySchema, userRealmConfigSchema, userResourceSchema, users };
|
|
2166
2397
|
//# sourceMappingURL=index.js.map
|