alepha 0.14.1 → 0.14.3
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 +3 -3
- 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 +784 -784
- 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 +57 -57
- 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 +165 -165
- 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 +583 -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 +281 -276
- 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 +778 -764
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +831 -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 +125 -125
- 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/batch/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/cache/core/index.js.map +1 -1
- package/dist/cli/index.d.ts +249 -218
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +951 -821
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +40 -0
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +97 -17
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +14 -18
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +29 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +21 -24
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +21 -24
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.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/lock/redis/index.js.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.browser.js +26 -5
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.d.ts +146 -121
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +49 -24
- package/dist/orm/index.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.js.map +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +6 -6
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +29 -29
- 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.js.map +1 -1
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +2 -0
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/cookies/index.browser.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +1 -1
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/health/index.d.ts +17 -17
- package/dist/server/helmet/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/multipart/index.js.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/security/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 +8 -3
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +12 -4
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.js.map +1 -1
- 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 +13 -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 +54 -16
- package/src/cli/apps/AlephaPackageBuilderCli.ts +2 -1
- package/src/cli/assets/appRouterTs.ts +1 -1
- package/src/cli/commands/{ViteCommands.ts → build.ts} +2 -105
- package/src/cli/commands/clean.ts +14 -0
- package/src/cli/commands/{DrizzleCommands.ts → db.ts} +10 -117
- package/src/cli/commands/{DeployCommands.ts → deploy.ts} +1 -1
- package/src/cli/commands/dev.ts +69 -0
- package/src/cli/commands/format.ts +17 -0
- package/src/cli/commands/gen/changelog.spec.ts +315 -0
- package/src/cli/commands/{ChangelogCommands.ts → gen/changelog.ts} +16 -31
- package/src/cli/commands/gen/openapi.ts +71 -0
- package/src/cli/commands/gen.ts +18 -0
- package/src/cli/commands/{CoreCommands.ts → init.ts} +4 -40
- package/src/cli/commands/lint.ts +17 -0
- package/src/cli/commands/root.ts +41 -0
- package/src/cli/commands/run.ts +24 -0
- package/src/cli/commands/test.ts +42 -0
- package/src/cli/commands/typecheck.ts +24 -0
- package/src/cli/commands/{VerifyCommands.ts → verify.ts} +1 -13
- package/src/cli/defineConfig.ts +10 -1
- package/src/cli/index.ts +17 -7
- package/src/cli/services/AlephaCliUtils.ts +71 -32
- 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/primitives/$command.spec.ts +1588 -0
- package/src/command/providers/CliProvider.ts +74 -24
- package/src/core/Alepha.ts +52 -4
- 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/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/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.browser.ts +1 -1
- package/src/orm/index.ts +10 -6
- 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/{PostgresTypeProvider.ts → DatabaseTypeProvider.ts} +25 -3
- 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 +378 -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 +942 -0
- package/src/server/compress/providers/ServerCompressProvider.spec.ts +31 -0
- package/src/server/compress/providers/ServerCompressProvider.ts +2 -0
- 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/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 +3 -1
- 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/swagger/__tests__/ui.spec.ts +52 -0
- package/src/server/swagger/primitives/$swagger.spec.ts +193 -0
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +18 -8
- 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/plugins/viteAlephaDev.ts +16 -4
- 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/commands/BiomeCommands.ts +0 -29
package/dist/api/users/index.js
CHANGED
|
@@ -2,20 +2,21 @@ import { $atom, $context, $hook, $inject, $module, Alepha, AlephaError, t } from
|
|
|
2
2
|
import { $notification, AlephaApiNotifications } from "alepha/api/notifications";
|
|
3
3
|
import { AlephaApiVerification } from "alepha/api/verifications";
|
|
4
4
|
import { AlephaEmail } from "alepha/email";
|
|
5
|
-
import {
|
|
5
|
+
import { AlephaServerCompress } from "alepha/server/compress";
|
|
6
|
+
import { AlephaServerHelmet } from "alepha/server/helmet";
|
|
6
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";
|
|
7
10
|
import { $logger } from "alepha/logger";
|
|
8
11
|
import { $bucket } from "alepha/bucket";
|
|
12
|
+
import { $client } from "alepha/server/links";
|
|
9
13
|
import { randomInt, randomUUID } from "node:crypto";
|
|
10
14
|
import { $cache } from "alepha/cache";
|
|
11
15
|
import { DateTimeProvider } from "alepha/datetime";
|
|
12
16
|
import { $realm, CryptoProvider, InvalidCredentialsError, SecurityProvider } from "alepha/security";
|
|
13
|
-
import { $client } from "alepha/server/links";
|
|
14
17
|
import { $authCredentials, $authGithub, $authGoogle, ServerAuthProvider, authenticationProviderSchema } from "alepha/server/auth";
|
|
15
18
|
import { FileSystemProvider } from "alepha/file";
|
|
16
|
-
import { AlephaApiAudits } from "alepha/api/audits";
|
|
17
19
|
import { AlephaApiFiles } from "alepha/api/files";
|
|
18
|
-
import { AlephaApiJobs } from "alepha/api/jobs";
|
|
19
20
|
|
|
20
21
|
//#region ../../src/api/users/schemas/identityQuerySchema.ts
|
|
21
22
|
const identityQuerySchema = t.extend(pageQuerySchema, {
|
|
@@ -29,11 +30,11 @@ const DEFAULT_USER_REALM_NAME = "default";
|
|
|
29
30
|
const users = $entity({
|
|
30
31
|
name: "users",
|
|
31
32
|
schema: t.object({
|
|
32
|
-
id:
|
|
33
|
-
version:
|
|
34
|
-
createdAt:
|
|
35
|
-
updatedAt:
|
|
36
|
-
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),
|
|
37
38
|
username: t.optional(t.shortText({
|
|
38
39
|
minLength: 3,
|
|
39
40
|
maxLength: 50,
|
|
@@ -41,12 +42,12 @@ const users = $entity({
|
|
|
41
42
|
})),
|
|
42
43
|
email: t.optional(t.string({ format: "email" })),
|
|
43
44
|
phoneNumber: t.optional(t.e164()),
|
|
44
|
-
roles:
|
|
45
|
+
roles: db.default(t.array(t.string()), []),
|
|
45
46
|
firstName: t.optional(t.string()),
|
|
46
47
|
lastName: t.optional(t.string()),
|
|
47
48
|
picture: t.optional(t.string()),
|
|
48
|
-
enabled:
|
|
49
|
-
emailVerified:
|
|
49
|
+
enabled: db.default(t.boolean(), true),
|
|
50
|
+
emailVerified: db.default(t.boolean(), false)
|
|
50
51
|
}),
|
|
51
52
|
indexes: [
|
|
52
53
|
{
|
|
@@ -69,11 +70,11 @@ const users = $entity({
|
|
|
69
70
|
const identities = $entity({
|
|
70
71
|
name: "identities",
|
|
71
72
|
schema: t.object({
|
|
72
|
-
id:
|
|
73
|
-
version:
|
|
74
|
-
createdAt:
|
|
75
|
-
updatedAt:
|
|
76
|
-
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),
|
|
77
78
|
password: t.optional(t.text()),
|
|
78
79
|
provider: t.text(),
|
|
79
80
|
providerUserId: t.optional(t.text()),
|
|
@@ -145,12 +146,12 @@ const realmAuthSettingsAtom = $atom({
|
|
|
145
146
|
const sessions = $entity({
|
|
146
147
|
name: "sessions",
|
|
147
148
|
schema: t.object({
|
|
148
|
-
id:
|
|
149
|
-
version:
|
|
150
|
-
createdAt:
|
|
151
|
-
updatedAt:
|
|
149
|
+
id: db.primaryKey(t.uuid()),
|
|
150
|
+
version: db.version(),
|
|
151
|
+
createdAt: db.createdAt(),
|
|
152
|
+
updatedAt: db.updatedAt(),
|
|
152
153
|
refreshToken: t.uuid(),
|
|
153
|
-
userId:
|
|
154
|
+
userId: db.ref(t.uuid(), () => users.cols.id),
|
|
154
155
|
expiresAt: t.datetime(),
|
|
155
156
|
ip: t.optional(t.text()),
|
|
156
157
|
userAgent: t.optional(t.object({
|
|
@@ -240,6 +241,7 @@ var UserRealmProvider = class {
|
|
|
240
241
|
var IdentityService = class {
|
|
241
242
|
log = $logger();
|
|
242
243
|
userRealmProvider = $inject(UserRealmProvider);
|
|
244
|
+
auditService = $inject(AuditService);
|
|
243
245
|
identities(userRealmName) {
|
|
244
246
|
return this.userRealmProvider.identityRepository(userRealmName);
|
|
245
247
|
}
|
|
@@ -293,14 +295,25 @@ var IdentityService = class {
|
|
|
293
295
|
provider: identity.provider,
|
|
294
296
|
userId: identity.userId
|
|
295
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
|
+
});
|
|
296
309
|
}
|
|
297
310
|
};
|
|
298
311
|
|
|
299
312
|
//#endregion
|
|
300
|
-
//#region ../../src/api/users/controllers/
|
|
301
|
-
var
|
|
313
|
+
//#region ../../src/api/users/controllers/AdminIdentityController.ts
|
|
314
|
+
var AdminIdentityController = class {
|
|
302
315
|
url = "/identities";
|
|
303
|
-
group = "identities";
|
|
316
|
+
group = "admin:identities";
|
|
304
317
|
identityService = $inject(IdentityService);
|
|
305
318
|
/**
|
|
306
319
|
* Find identities with pagination and filtering.
|
|
@@ -311,7 +324,7 @@ var IdentityController = class {
|
|
|
311
324
|
description: "Find identities with pagination and filtering",
|
|
312
325
|
schema: {
|
|
313
326
|
query: t.extend(identityQuerySchema, { userRealmName: t.optional(t.string()) }),
|
|
314
|
-
response:
|
|
327
|
+
response: t.page(identityResourceSchema)
|
|
315
328
|
},
|
|
316
329
|
handler: ({ query }) => {
|
|
317
330
|
const { userRealmName, ...q } = query;
|
|
@@ -437,10 +450,10 @@ var SessionCrudService = class {
|
|
|
437
450
|
};
|
|
438
451
|
|
|
439
452
|
//#endregion
|
|
440
|
-
//#region ../../src/api/users/controllers/
|
|
441
|
-
var
|
|
453
|
+
//#region ../../src/api/users/controllers/AdminSessionController.ts
|
|
454
|
+
var AdminSessionController = class {
|
|
442
455
|
url = "/sessions";
|
|
443
|
-
group = "sessions";
|
|
456
|
+
group = "admin:sessions";
|
|
444
457
|
sessionService = $inject(SessionCrudService);
|
|
445
458
|
/**
|
|
446
459
|
* Find sessions with pagination and filtering.
|
|
@@ -451,7 +464,7 @@ var SessionController = class {
|
|
|
451
464
|
description: "Find sessions with pagination and filtering",
|
|
452
465
|
schema: {
|
|
453
466
|
query: t.extend(sessionQuerySchema, { userRealmName: t.optional(t.string()) }),
|
|
454
|
-
response:
|
|
467
|
+
response: t.page(sessionResourceSchema)
|
|
455
468
|
},
|
|
456
469
|
handler: ({ query }) => {
|
|
457
470
|
const { userRealmName, ...q } = query;
|
|
@@ -495,92 +508,10 @@ var SessionController = class {
|
|
|
495
508
|
});
|
|
496
509
|
};
|
|
497
510
|
|
|
498
|
-
//#endregion
|
|
499
|
-
//#region ../../src/api/users/schemas/completePasswordResetRequestSchema.ts
|
|
500
|
-
/**
|
|
501
|
-
* Request schema for completing a password reset.
|
|
502
|
-
*
|
|
503
|
-
* Requires the intent ID from Phase 1, the verification code,
|
|
504
|
-
* and the new password.
|
|
505
|
-
*/
|
|
506
|
-
const completePasswordResetRequestSchema = t.object({
|
|
507
|
-
intentId: t.uuid({ description: "The intent ID from createPasswordResetIntent" }),
|
|
508
|
-
code: t.string({ description: "6-digit verification code sent via email" }),
|
|
509
|
-
newPassword: t.string({
|
|
510
|
-
minLength: 8,
|
|
511
|
-
description: "New password (minimum 8 characters)"
|
|
512
|
-
})
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
//#endregion
|
|
516
|
-
//#region ../../src/api/users/schemas/completeRegistrationRequestSchema.ts
|
|
517
|
-
const completeRegistrationRequestSchema = t.object({
|
|
518
|
-
intentId: t.uuid({ description: "The registration intent ID from the first phase" }),
|
|
519
|
-
emailCode: t.optional(t.string({ description: "Email verification code (if email verification required)" })),
|
|
520
|
-
phoneCode: t.optional(t.string({ description: "Phone verification code (if phone verification required)" })),
|
|
521
|
-
captchaToken: t.optional(t.string({ description: "Captcha token (if captcha required)" }))
|
|
522
|
-
});
|
|
523
|
-
|
|
524
511
|
//#endregion
|
|
525
512
|
//#region ../../src/api/users/schemas/createUserSchema.ts
|
|
526
513
|
const createUserSchema = t.omit(users.insertSchema, ["realm"]);
|
|
527
514
|
|
|
528
|
-
//#endregion
|
|
529
|
-
//#region ../../src/api/users/schemas/passwordResetIntentResponseSchema.ts
|
|
530
|
-
/**
|
|
531
|
-
* Response schema for password reset intent creation.
|
|
532
|
-
*
|
|
533
|
-
* Contains the intent ID needed for Phase 2 completion,
|
|
534
|
-
* along with expiration time.
|
|
535
|
-
*/
|
|
536
|
-
const passwordResetIntentResponseSchema = t.object({
|
|
537
|
-
intentId: t.uuid({ description: "Unique identifier for this password reset intent" }),
|
|
538
|
-
expiresAt: t.datetime({ description: "ISO timestamp when this intent expires" })
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
//#endregion
|
|
542
|
-
//#region ../../src/api/users/schemas/registerQuerySchema.ts
|
|
543
|
-
/**
|
|
544
|
-
* Schema for user registration query parameters.
|
|
545
|
-
* Allows specifying a custom user realm.
|
|
546
|
-
*/
|
|
547
|
-
const registerQuerySchema = t.object({ userRealmName: t.optional(t.text({ description: "The user realm to register the user in (defaults to 'default')" })) });
|
|
548
|
-
|
|
549
|
-
//#endregion
|
|
550
|
-
//#region ../../src/api/users/schemas/registerRequestSchema.ts
|
|
551
|
-
/**
|
|
552
|
-
* Schema for user registration request body.
|
|
553
|
-
* Password is always required, other fields depend on realm settings.
|
|
554
|
-
*/
|
|
555
|
-
const registerRequestSchema = t.object({
|
|
556
|
-
password: t.string({
|
|
557
|
-
minLength: 8,
|
|
558
|
-
description: "Password for the account"
|
|
559
|
-
}),
|
|
560
|
-
username: t.optional(t.string({
|
|
561
|
-
minLength: 3,
|
|
562
|
-
description: "Unique username for the account"
|
|
563
|
-
})),
|
|
564
|
-
email: t.optional(t.string({
|
|
565
|
-
format: "email",
|
|
566
|
-
description: "User's email address"
|
|
567
|
-
})),
|
|
568
|
-
phoneNumber: t.optional(t.string({ description: "User's phone number" })),
|
|
569
|
-
firstName: t.optional(t.string({ description: "User's first name" })),
|
|
570
|
-
lastName: t.optional(t.string({ description: "User's last name" })),
|
|
571
|
-
picture: t.optional(t.string({ description: "User's profile picture URL" }))
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
//#endregion
|
|
575
|
-
//#region ../../src/api/users/schemas/registrationIntentResponseSchema.ts
|
|
576
|
-
const registrationIntentResponseSchema = t.object({
|
|
577
|
-
intentId: t.uuid({ description: "Unique identifier for the registration intent" }),
|
|
578
|
-
expectCaptcha: t.boolean({ description: "Whether captcha verification is required" }),
|
|
579
|
-
expectEmailVerification: t.boolean({ description: "Whether email verification is required" }),
|
|
580
|
-
expectPhoneVerification: t.boolean({ description: "Whether phone verification is required" }),
|
|
581
|
-
expiresAt: t.datetime({ description: "When the registration intent expires" })
|
|
582
|
-
});
|
|
583
|
-
|
|
584
515
|
//#endregion
|
|
585
516
|
//#region ../../src/api/users/schemas/updateUserSchema.ts
|
|
586
517
|
const updateUserSchema = t.partial(t.omit(users.insertSchema, [
|
|
@@ -736,136 +667,578 @@ var UserNotifications = class {
|
|
|
736
667
|
};
|
|
737
668
|
|
|
738
669
|
//#endregion
|
|
739
|
-
//#region ../../src/api/users/services/
|
|
740
|
-
|
|
741
|
-
var CredentialService = class {
|
|
670
|
+
//#region ../../src/api/users/services/UserService.ts
|
|
671
|
+
var UserService = class {
|
|
742
672
|
log = $logger();
|
|
743
|
-
cryptoProvider = $inject(CryptoProvider);
|
|
744
|
-
dateTimeProvider = $inject(DateTimeProvider);
|
|
745
673
|
verificationController = $client();
|
|
746
674
|
userNotifications = $inject(UserNotifications);
|
|
747
675
|
userRealmProvider = $inject(UserRealmProvider);
|
|
748
|
-
|
|
749
|
-
name: "password-reset-intents",
|
|
750
|
-
ttl: [INTENT_TTL_MINUTES$1, "minutes"]
|
|
751
|
-
});
|
|
676
|
+
auditService = $inject(AuditService);
|
|
752
677
|
users(userRealmName) {
|
|
753
678
|
return this.userRealmProvider.userRepository(userRealmName);
|
|
754
679
|
}
|
|
755
|
-
sessions(userRealmName) {
|
|
756
|
-
return this.userRealmProvider.sessionRepository(userRealmName);
|
|
757
|
-
}
|
|
758
|
-
identities(userRealmName) {
|
|
759
|
-
return this.userRealmProvider.identityRepository(userRealmName);
|
|
760
|
-
}
|
|
761
680
|
/**
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
* @param email - User's email address
|
|
768
|
-
* @param userRealmName - Optional realm name
|
|
769
|
-
* @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").
|
|
770
686
|
*/
|
|
771
|
-
async
|
|
772
|
-
this.log.trace("
|
|
687
|
+
async requestEmailVerification(email, userRealmName, method = "code", verifyUrl) {
|
|
688
|
+
this.log.trace("Requesting email verification", {
|
|
773
689
|
email,
|
|
774
|
-
userRealmName
|
|
690
|
+
userRealmName,
|
|
691
|
+
method
|
|
775
692
|
});
|
|
776
|
-
const intentId = randomUUID();
|
|
777
|
-
const expiresAt = this.dateTimeProvider.now().add(INTENT_TTL_MINUTES$1, "minutes").toISOString();
|
|
778
693
|
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } }).catch(() => void 0);
|
|
779
694
|
if (!user) {
|
|
780
|
-
this.log.debug("
|
|
781
|
-
return
|
|
782
|
-
intentId,
|
|
783
|
-
expiresAt
|
|
784
|
-
};
|
|
695
|
+
this.log.debug("Email verification requested for non-existent user", { email });
|
|
696
|
+
return true;
|
|
785
697
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
return {
|
|
793
|
-
intentId,
|
|
794
|
-
expiresAt
|
|
795
|
-
};
|
|
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;
|
|
796
704
|
}
|
|
797
705
|
try {
|
|
798
706
|
const verification = await this.verificationController.requestVerificationCode({
|
|
799
|
-
params: { type:
|
|
707
|
+
params: { type: method },
|
|
800
708
|
body: { target: email }
|
|
801
709
|
});
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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", {
|
|
805
724
|
email,
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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", {
|
|
811
743
|
email,
|
|
812
|
-
|
|
813
|
-
identityId: identity.id,
|
|
814
|
-
realmName: userRealmName,
|
|
815
|
-
expiresAt
|
|
816
|
-
};
|
|
817
|
-
await this.intentCache.set(intentId, intent);
|
|
818
|
-
this.log.info("Password reset intent created", {
|
|
819
|
-
intentId,
|
|
820
|
-
userId: user.id,
|
|
821
|
-
email
|
|
744
|
+
error
|
|
822
745
|
});
|
|
823
|
-
} catch (error) {
|
|
824
|
-
this.log.warn("Failed to create password reset verification", error);
|
|
825
746
|
}
|
|
826
|
-
return
|
|
827
|
-
intentId,
|
|
828
|
-
expiresAt
|
|
829
|
-
};
|
|
747
|
+
return true;
|
|
830
748
|
}
|
|
831
749
|
/**
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
* Validates the verification code, updates the password,
|
|
835
|
-
* and invalidates all existing sessions.
|
|
836
|
-
*
|
|
837
|
-
* @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.
|
|
838
752
|
*/
|
|
839
|
-
async
|
|
840
|
-
this.log.trace("
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
status: 410,
|
|
846
|
-
message: "Invalid or expired password reset intent"
|
|
847
|
-
});
|
|
848
|
-
}
|
|
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";
|
|
849
759
|
if ((await this.verificationController.validateVerificationCode({
|
|
850
|
-
params: { type
|
|
760
|
+
params: { type },
|
|
851
761
|
body: {
|
|
852
|
-
target:
|
|
853
|
-
token
|
|
762
|
+
target: email,
|
|
763
|
+
token
|
|
854
764
|
}
|
|
855
765
|
}).catch(() => {
|
|
856
|
-
this.log.warn("Invalid verification
|
|
857
|
-
|
|
858
|
-
|
|
766
|
+
this.log.warn("Invalid email verification token", {
|
|
767
|
+
email,
|
|
768
|
+
type
|
|
859
769
|
});
|
|
860
|
-
throw new BadRequestError("Invalid or expired verification
|
|
770
|
+
throw new BadRequestError("Invalid or expired verification token");
|
|
861
771
|
})).alreadyVerified) {
|
|
862
|
-
this.log.warn("
|
|
863
|
-
|
|
864
|
-
email: intent.email
|
|
865
|
-
});
|
|
866
|
-
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");
|
|
867
774
|
}
|
|
868
|
-
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);
|
|
869
1242
|
const hashedPassword = await this.cryptoProvider.hashPassword(body.newPassword);
|
|
870
1243
|
await this.identities(intent.realmName).updateById(intent.identityId, { password: hashedPassword });
|
|
871
1244
|
await this.sessions(intent.realmName).deleteMany({ userId: { eq: intent.userId } });
|
|
@@ -873,6 +1246,23 @@ var CredentialService = class {
|
|
|
873
1246
|
userId: intent.userId,
|
|
874
1247
|
email: intent.email
|
|
875
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
|
+
});
|
|
876
1266
|
}
|
|
877
1267
|
/**
|
|
878
1268
|
* @deprecated Use createPasswordResetIntent instead
|
|
@@ -915,6 +1305,23 @@ var CredentialService = class {
|
|
|
915
1305
|
const hashedPassword = await this.cryptoProvider.hashPassword(newPassword);
|
|
916
1306
|
await this.identities(userRealmName).updateById(identity.id, { password: hashedPassword });
|
|
917
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
|
+
});
|
|
918
1325
|
}
|
|
919
1326
|
};
|
|
920
1327
|
|
|
@@ -928,6 +1335,7 @@ var RegistrationService = class {
|
|
|
928
1335
|
verificationController = $client();
|
|
929
1336
|
userNotifications = $inject(UserNotifications);
|
|
930
1337
|
userRealmProvider = $inject(UserRealmProvider);
|
|
1338
|
+
auditService = $inject(AuditService);
|
|
931
1339
|
intentCache = $cache({
|
|
932
1340
|
name: "registration-intents",
|
|
933
1341
|
ttl: [INTENT_TTL_MINUTES, "minutes"]
|
|
@@ -1069,6 +1477,20 @@ var RegistrationService = class {
|
|
|
1069
1477
|
email: user.email,
|
|
1070
1478
|
username: user.username
|
|
1071
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
|
+
});
|
|
1072
1494
|
return user;
|
|
1073
1495
|
}
|
|
1074
1496
|
/**
|
|
@@ -1090,317 +1512,95 @@ var RegistrationService = class {
|
|
|
1090
1512
|
}
|
|
1091
1513
|
if (body.phoneNumber) {
|
|
1092
1514
|
if (await userRepository.findOne({ where: { phoneNumber: { eq: body.phoneNumber } } }).catch(() => void 0)) {
|
|
1093
|
-
this.log.debug("Phone number already taken", { phoneNumber: body.phoneNumber });
|
|
1094
|
-
throw new ConflictError("User with this phone number already exists");
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
/**
|
|
1099
|
-
* Send email verification code.
|
|
1100
|
-
*/
|
|
1101
|
-
async sendEmailVerification(email) {
|
|
1102
|
-
this.log.debug("Sending email verification code", { email });
|
|
1103
|
-
try {
|
|
1104
|
-
const verification = await this.verificationController.requestVerificationCode({
|
|
1105
|
-
params: { type: "code" },
|
|
1106
|
-
body: { target: email }
|
|
1107
|
-
});
|
|
1108
|
-
await this.userNotifications.emailVerification.push({
|
|
1109
|
-
contact: email,
|
|
1110
|
-
variables: {
|
|
1111
|
-
email,
|
|
1112
|
-
code: verification.token,
|
|
1113
|
-
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1114
|
-
}
|
|
1115
|
-
});
|
|
1116
|
-
this.log.debug("Email verification code sent", { email });
|
|
1117
|
-
} catch (error) {
|
|
1118
|
-
this.log.warn("Failed to send email verification code", error);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
/**
|
|
1122
|
-
* Send phone verification code.
|
|
1123
|
-
*/
|
|
1124
|
-
async sendPhoneVerification(phoneNumber) {
|
|
1125
|
-
this.log.debug("Sending phone verification code", { phoneNumber });
|
|
1126
|
-
try {
|
|
1127
|
-
const verification = await this.verificationController.requestVerificationCode({
|
|
1128
|
-
params: { type: "code" },
|
|
1129
|
-
body: { target: phoneNumber }
|
|
1130
|
-
});
|
|
1131
|
-
await this.userNotifications.phoneVerification.push({
|
|
1132
|
-
contact: phoneNumber,
|
|
1133
|
-
variables: {
|
|
1134
|
-
phoneNumber,
|
|
1135
|
-
code: verification.token,
|
|
1136
|
-
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1137
|
-
}
|
|
1138
|
-
});
|
|
1139
|
-
this.log.debug("Phone verification code sent", { phoneNumber });
|
|
1140
|
-
} catch (error) {
|
|
1141
|
-
this.log.warn("Failed to send phone verification code", {
|
|
1142
|
-
phoneNumber,
|
|
1143
|
-
error
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
/**
|
|
1148
|
-
* Verify email code using verification service.
|
|
1149
|
-
*/
|
|
1150
|
-
async verifyEmailCode(email, code) {
|
|
1151
|
-
if ((await this.verificationController.validateVerificationCode({
|
|
1152
|
-
params: { type: "code" },
|
|
1153
|
-
body: {
|
|
1154
|
-
target: email,
|
|
1155
|
-
token: code
|
|
1156
|
-
}
|
|
1157
|
-
}).catch(() => {
|
|
1158
|
-
this.log.warn("Invalid email verification code", { email });
|
|
1159
|
-
throw new BadRequestError("Invalid or expired email verification code");
|
|
1160
|
-
})).alreadyVerified) {
|
|
1161
|
-
this.log.warn("Email verification code already used", { email });
|
|
1162
|
-
throw new BadRequestError("Email verification code has already been used");
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
/**
|
|
1166
|
-
* Verify phone code using verification service.
|
|
1167
|
-
*/
|
|
1168
|
-
async verifyPhoneCode(phoneNumber, code) {
|
|
1169
|
-
if ((await this.verificationController.validateVerificationCode({
|
|
1170
|
-
params: { type: "code" },
|
|
1171
|
-
body: {
|
|
1172
|
-
target: phoneNumber,
|
|
1173
|
-
token: code
|
|
1174
|
-
}
|
|
1175
|
-
}).catch(() => {
|
|
1176
|
-
this.log.warn("Invalid phone verification code", { phoneNumber });
|
|
1177
|
-
throw new BadRequestError("Invalid or expired phone verification code");
|
|
1178
|
-
})).alreadyVerified) {
|
|
1179
|
-
this.log.warn("Phone verification code already used", { phoneNumber });
|
|
1180
|
-
throw new BadRequestError("Phone verification code has already been used");
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
};
|
|
1184
|
-
|
|
1185
|
-
//#endregion
|
|
1186
|
-
//#region ../../src/api/users/services/UserService.ts
|
|
1187
|
-
var UserService = class {
|
|
1188
|
-
log = $logger();
|
|
1189
|
-
verificationController = $client();
|
|
1190
|
-
userNotifications = $inject(UserNotifications);
|
|
1191
|
-
userRealmProvider = $inject(UserRealmProvider);
|
|
1192
|
-
users(userRealmName) {
|
|
1193
|
-
return this.userRealmProvider.userRepository(userRealmName);
|
|
1194
|
-
}
|
|
1195
|
-
/**
|
|
1196
|
-
* Request email verification for a user.
|
|
1197
|
-
* @param email - The email address to verify.
|
|
1198
|
-
* @param userRealmName - Optional realm name.
|
|
1199
|
-
* @param method - The verification method: "code" (default) or "link".
|
|
1200
|
-
* @param verifyUrl - Base URL for verification link (required when method is "link").
|
|
1201
|
-
*/
|
|
1202
|
-
async requestEmailVerification(email, userRealmName, method = "code", verifyUrl) {
|
|
1203
|
-
this.log.trace("Requesting email verification", {
|
|
1204
|
-
email,
|
|
1205
|
-
userRealmName,
|
|
1206
|
-
method
|
|
1207
|
-
});
|
|
1208
|
-
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } }).catch(() => void 0);
|
|
1209
|
-
if (!user) {
|
|
1210
|
-
this.log.debug("Email verification requested for non-existent user", { email });
|
|
1211
|
-
return true;
|
|
1212
|
-
}
|
|
1213
|
-
if (user.emailVerified) {
|
|
1214
|
-
this.log.debug("Email verification requested for already verified user", {
|
|
1215
|
-
email,
|
|
1216
|
-
userId: user.id
|
|
1217
|
-
});
|
|
1218
|
-
return true;
|
|
1219
|
-
}
|
|
1220
|
-
try {
|
|
1221
|
-
const verification = await this.verificationController.requestVerificationCode({
|
|
1222
|
-
params: { type: method },
|
|
1223
|
-
body: { target: email }
|
|
1224
|
-
});
|
|
1225
|
-
if (method === "link") {
|
|
1226
|
-
const url = new URL(verifyUrl || "/verify-email", "http://localhost");
|
|
1227
|
-
url.searchParams.set("email", email);
|
|
1228
|
-
url.searchParams.set("token", verification.token);
|
|
1229
|
-
const fullVerifyUrl = verifyUrl ? `${verifyUrl}${url.search}` : url.pathname + url.search;
|
|
1230
|
-
await this.userNotifications.emailVerificationLink.push({
|
|
1231
|
-
contact: email,
|
|
1232
|
-
variables: {
|
|
1233
|
-
email,
|
|
1234
|
-
verifyUrl: fullVerifyUrl,
|
|
1235
|
-
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1236
|
-
}
|
|
1237
|
-
});
|
|
1238
|
-
this.log.debug("Email verification link sent", {
|
|
1239
|
-
email,
|
|
1240
|
-
userId: user.id
|
|
1241
|
-
});
|
|
1242
|
-
} else {
|
|
1243
|
-
await this.userNotifications.emailVerification.push({
|
|
1244
|
-
contact: email,
|
|
1245
|
-
variables: {
|
|
1246
|
-
email,
|
|
1247
|
-
code: verification.token,
|
|
1248
|
-
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1249
|
-
}
|
|
1250
|
-
});
|
|
1251
|
-
this.log.debug("Email verification code sent", {
|
|
1252
|
-
email,
|
|
1253
|
-
userId: user.id
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
|
-
} catch (error) {
|
|
1257
|
-
this.log.warn("Failed to send email verification", {
|
|
1258
|
-
email,
|
|
1259
|
-
error
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
return true;
|
|
1263
|
-
}
|
|
1264
|
-
/**
|
|
1265
|
-
* Verify a user's email using a valid verification token.
|
|
1266
|
-
* Supports both code (6-digit) and link (UUID) verification tokens.
|
|
1267
|
-
*/
|
|
1268
|
-
async verifyEmail(email, token, userRealmName) {
|
|
1269
|
-
this.log.trace("Verifying email", {
|
|
1270
|
-
email,
|
|
1271
|
-
userRealmName
|
|
1272
|
-
});
|
|
1273
|
-
const type = /^\d{6}$/.test(token) ? "code" : "link";
|
|
1274
|
-
if ((await this.verificationController.validateVerificationCode({
|
|
1275
|
-
params: { type },
|
|
1276
|
-
body: {
|
|
1277
|
-
target: email,
|
|
1278
|
-
token
|
|
1279
|
-
}
|
|
1280
|
-
}).catch(() => {
|
|
1281
|
-
this.log.warn("Invalid email verification token", {
|
|
1282
|
-
email,
|
|
1283
|
-
type
|
|
1284
|
-
});
|
|
1285
|
-
throw new BadRequestError("Invalid or expired verification token");
|
|
1286
|
-
})).alreadyVerified) {
|
|
1287
|
-
this.log.warn("Email verification token already used", { email });
|
|
1288
|
-
throw new BadRequestError("Invalid or expired verification token");
|
|
1289
|
-
}
|
|
1290
|
-
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } });
|
|
1291
|
-
await this.users(userRealmName).updateById(user.id, { emailVerified: true });
|
|
1292
|
-
this.log.info("Email verified", {
|
|
1293
|
-
email,
|
|
1294
|
-
userId: user.id,
|
|
1295
|
-
type
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
/**
|
|
1299
|
-
* Check if an email is verified.
|
|
1300
|
-
*/
|
|
1301
|
-
async isEmailVerified(email, userRealmName) {
|
|
1302
|
-
this.log.trace("Checking if email is verified", {
|
|
1303
|
-
email,
|
|
1304
|
-
userRealmName
|
|
1305
|
-
});
|
|
1306
|
-
return (await this.users(userRealmName).findOne({ where: { email: { eq: email } } }).catch(() => void 0))?.emailVerified ?? false;
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Find users with pagination and filtering.
|
|
1310
|
-
*/
|
|
1311
|
-
async findUsers(q = {}, userRealmName) {
|
|
1312
|
-
this.log.trace("Finding users", {
|
|
1313
|
-
query: q,
|
|
1314
|
-
userRealmName
|
|
1315
|
-
});
|
|
1316
|
-
q.sort ??= "-createdAt";
|
|
1317
|
-
const where = this.users(userRealmName).createQueryWhere();
|
|
1318
|
-
if (q.email) where.email = { like: q.email };
|
|
1319
|
-
if (q.enabled !== void 0) where.enabled = { eq: q.enabled };
|
|
1320
|
-
if (q.emailVerified !== void 0) where.emailVerified = { eq: q.emailVerified };
|
|
1321
|
-
if (q.roles) where.roles = { arrayContains: q.roles };
|
|
1322
|
-
if (q.query) Object.assign(where, parseQueryString(q.query));
|
|
1323
|
-
const result = await this.users(userRealmName).paginate(q, { where }, { count: true });
|
|
1324
|
-
this.log.debug("Users found", {
|
|
1325
|
-
count: result.content.length,
|
|
1326
|
-
total: result.page.totalElements
|
|
1327
|
-
});
|
|
1328
|
-
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
|
+
}
|
|
1329
1519
|
}
|
|
1330
1520
|
/**
|
|
1331
|
-
*
|
|
1521
|
+
* Send email verification code.
|
|
1332
1522
|
*/
|
|
1333
|
-
async
|
|
1334
|
-
this.log.
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
+
}
|
|
1339
1542
|
}
|
|
1340
1543
|
/**
|
|
1341
|
-
*
|
|
1544
|
+
* Send phone verification code.
|
|
1342
1545
|
*/
|
|
1343
|
-
async
|
|
1344
|
-
this.log.
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
this.log.debug("Phone number already taken", { phoneNumber: data.phoneNumber });
|
|
1365
|
-
throw new BadRequestError("User with this phone number already exists");
|
|
1366
|
-
}
|
|
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
|
+
});
|
|
1367
1567
|
}
|
|
1368
|
-
const user = await this.users(userRealmName).create({
|
|
1369
|
-
...data,
|
|
1370
|
-
roles: data.roles ?? ["user"],
|
|
1371
|
-
realm: realm.name
|
|
1372
|
-
});
|
|
1373
|
-
this.log.info("User created", {
|
|
1374
|
-
userId: user.id,
|
|
1375
|
-
username: user.username,
|
|
1376
|
-
email: user.email
|
|
1377
|
-
});
|
|
1378
|
-
return user;
|
|
1379
1568
|
}
|
|
1380
1569
|
/**
|
|
1381
|
-
*
|
|
1570
|
+
* Verify email code using verification service.
|
|
1382
1571
|
*/
|
|
1383
|
-
async
|
|
1384
|
-
this.
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
+
}
|
|
1392
1586
|
}
|
|
1393
1587
|
/**
|
|
1394
|
-
*
|
|
1588
|
+
* Verify phone code using verification service.
|
|
1395
1589
|
*/
|
|
1396
|
-
async
|
|
1397
|
-
this.
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
+
}
|
|
1404
1604
|
}
|
|
1405
1605
|
};
|
|
1406
1606
|
|
|
@@ -1417,6 +1617,7 @@ var UserController = class {
|
|
|
1417
1617
|
* Validates data, creates verification sessions, and stores intent in cache.
|
|
1418
1618
|
*/
|
|
1419
1619
|
createRegistrationIntent = $action({
|
|
1620
|
+
group: this.group,
|
|
1420
1621
|
method: "POST",
|
|
1421
1622
|
path: `${this.url}/register`,
|
|
1422
1623
|
secure: false,
|
|
@@ -1428,55 +1629,11 @@ var UserController = class {
|
|
|
1428
1629
|
handler: ({ body, query }) => this.registrationService.createRegistrationIntent(body, query.userRealmName)
|
|
1429
1630
|
});
|
|
1430
1631
|
/**
|
|
1431
|
-
* Find users with pagination and filtering.
|
|
1432
|
-
*/
|
|
1433
|
-
findUsers = $action({
|
|
1434
|
-
path: this.url,
|
|
1435
|
-
group: this.group,
|
|
1436
|
-
description: "Find users with pagination and filtering",
|
|
1437
|
-
schema: {
|
|
1438
|
-
query: t.extend(userQuerySchema, { userRealmName: t.optional(t.string()) }),
|
|
1439
|
-
response: pg.page(userResourceSchema)
|
|
1440
|
-
},
|
|
1441
|
-
handler: ({ query }) => {
|
|
1442
|
-
const { userRealmName, ...q } = query;
|
|
1443
|
-
return this.userService.findUsers(q, userRealmName);
|
|
1444
|
-
}
|
|
1445
|
-
});
|
|
1446
|
-
/**
|
|
1447
|
-
* Get a user by ID.
|
|
1448
|
-
*/
|
|
1449
|
-
getUser = $action({
|
|
1450
|
-
path: `${this.url}/:id`,
|
|
1451
|
-
group: this.group,
|
|
1452
|
-
description: "Get a user by ID",
|
|
1453
|
-
schema: {
|
|
1454
|
-
params: t.object({ id: t.uuid() }),
|
|
1455
|
-
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1456
|
-
response: userResourceSchema
|
|
1457
|
-
},
|
|
1458
|
-
handler: ({ params, query }) => this.userService.getUserById(params.id, query.userRealmName)
|
|
1459
|
-
});
|
|
1460
|
-
/**
|
|
1461
|
-
* Create a new user.
|
|
1462
|
-
*/
|
|
1463
|
-
createUser = $action({
|
|
1464
|
-
method: "POST",
|
|
1465
|
-
path: this.url,
|
|
1466
|
-
group: this.group,
|
|
1467
|
-
description: "Create a new user",
|
|
1468
|
-
schema: {
|
|
1469
|
-
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1470
|
-
body: createUserSchema,
|
|
1471
|
-
response: userResourceSchema
|
|
1472
|
-
},
|
|
1473
|
-
handler: ({ body, query }) => this.userService.createUser(body, query.userRealmName)
|
|
1474
|
-
});
|
|
1475
|
-
/**
|
|
1476
1632
|
* Phase 2: Complete registration using an intent.
|
|
1477
1633
|
* Validates verification codes and creates the user.
|
|
1478
1634
|
*/
|
|
1479
1635
|
createUserFromIntent = $action({
|
|
1636
|
+
group: this.group,
|
|
1480
1637
|
method: "POST",
|
|
1481
1638
|
path: `${this.url}/register/complete`,
|
|
1482
1639
|
secure: false,
|
|
@@ -1487,47 +1644,11 @@ var UserController = class {
|
|
|
1487
1644
|
handler: ({ body }) => this.registrationService.completeRegistration(body)
|
|
1488
1645
|
});
|
|
1489
1646
|
/**
|
|
1490
|
-
* Update a user.
|
|
1491
|
-
*/
|
|
1492
|
-
updateUser = $action({
|
|
1493
|
-
method: "PATCH",
|
|
1494
|
-
path: `${this.url}/:id`,
|
|
1495
|
-
group: this.group,
|
|
1496
|
-
description: "Update a user",
|
|
1497
|
-
schema: {
|
|
1498
|
-
params: t.object({ id: t.uuid() }),
|
|
1499
|
-
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1500
|
-
body: updateUserSchema,
|
|
1501
|
-
response: userResourceSchema
|
|
1502
|
-
},
|
|
1503
|
-
handler: ({ params, body, query }) => this.userService.updateUser(params.id, body, query.userRealmName)
|
|
1504
|
-
});
|
|
1505
|
-
/**
|
|
1506
|
-
* Delete a user.
|
|
1507
|
-
*/
|
|
1508
|
-
deleteUser = $action({
|
|
1509
|
-
method: "DELETE",
|
|
1510
|
-
path: `${this.url}/:id`,
|
|
1511
|
-
group: this.group,
|
|
1512
|
-
description: "Delete a user",
|
|
1513
|
-
schema: {
|
|
1514
|
-
params: t.object({ id: t.uuid() }),
|
|
1515
|
-
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1516
|
-
response: okSchema
|
|
1517
|
-
},
|
|
1518
|
-
handler: async ({ params, query }) => {
|
|
1519
|
-
await this.userService.deleteUser(params.id, query.userRealmName);
|
|
1520
|
-
return {
|
|
1521
|
-
ok: true,
|
|
1522
|
-
id: params.id
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
});
|
|
1526
|
-
/**
|
|
1527
1647
|
* Phase 1: Create a password reset intent.
|
|
1528
1648
|
* Validates email, sends verification code, and stores intent in cache.
|
|
1529
1649
|
*/
|
|
1530
1650
|
createPasswordResetIntent = $action({
|
|
1651
|
+
group: this.group,
|
|
1531
1652
|
method: "POST",
|
|
1532
1653
|
path: `${this.url}/password-reset`,
|
|
1533
1654
|
secure: false,
|
|
@@ -1543,6 +1664,7 @@ var UserController = class {
|
|
|
1543
1664
|
* Validates verification code, updates password, and invalidates sessions.
|
|
1544
1665
|
*/
|
|
1545
1666
|
completePasswordReset = $action({
|
|
1667
|
+
group: this.group,
|
|
1546
1668
|
method: "POST",
|
|
1547
1669
|
path: `${this.url}/password-reset/complete`,
|
|
1548
1670
|
secure: false,
|
|
@@ -1725,6 +1847,7 @@ const userRealmConfigSchema = t.object({
|
|
|
1725
1847
|
*/
|
|
1726
1848
|
var UserRealmController = class {
|
|
1727
1849
|
url = "/realms";
|
|
1850
|
+
group = "realms";
|
|
1728
1851
|
userRealmProvider = $inject(UserRealmProvider);
|
|
1729
1852
|
serverAuthProvider = $inject(ServerAuthProvider);
|
|
1730
1853
|
/**
|
|
@@ -1732,6 +1855,7 @@ var UserRealmController = class {
|
|
|
1732
1855
|
* This endpoint is not exposed in the API documentation.
|
|
1733
1856
|
*/
|
|
1734
1857
|
getRealmConfig = $action({
|
|
1858
|
+
group: this.group,
|
|
1735
1859
|
method: "GET",
|
|
1736
1860
|
path: `${this.url}/config`,
|
|
1737
1861
|
secure: false,
|
|
@@ -1753,6 +1877,7 @@ var UserRealmController = class {
|
|
|
1753
1877
|
}
|
|
1754
1878
|
});
|
|
1755
1879
|
checkUsernameAvailability = $action({
|
|
1880
|
+
group: this.group,
|
|
1756
1881
|
path: `${this.url}/check-username`,
|
|
1757
1882
|
secure: false,
|
|
1758
1883
|
schema: {
|
|
@@ -1777,6 +1902,7 @@ var SessionService = class {
|
|
|
1777
1902
|
log = $logger();
|
|
1778
1903
|
userRealmProvider = $inject(UserRealmProvider);
|
|
1779
1904
|
fileController = $client();
|
|
1905
|
+
auditService = $inject(AuditService);
|
|
1780
1906
|
users(userRealmName) {
|
|
1781
1907
|
return this.userRealmProvider.userRepository(userRealmName);
|
|
1782
1908
|
}
|
|
@@ -1816,6 +1942,14 @@ var SessionService = class {
|
|
|
1816
1942
|
username,
|
|
1817
1943
|
realm: name
|
|
1818
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
|
+
});
|
|
1819
1953
|
throw new InvalidCredentialsError();
|
|
1820
1954
|
}
|
|
1821
1955
|
const user = await users$1.findOne({ where }).catch(() => void 0);
|
|
@@ -1825,6 +1959,14 @@ var SessionService = class {
|
|
|
1825
1959
|
username,
|
|
1826
1960
|
realm: name
|
|
1827
1961
|
});
|
|
1962
|
+
await this.auditService.recordAuth("login_failed", {
|
|
1963
|
+
userRealm: name,
|
|
1964
|
+
description: "User not found",
|
|
1965
|
+
metadata: {
|
|
1966
|
+
provider,
|
|
1967
|
+
username
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1828
1970
|
throw new InvalidCredentialsError();
|
|
1829
1971
|
}
|
|
1830
1972
|
const identity = await identities$1.findOne({ where: {
|
|
@@ -1847,8 +1989,28 @@ var SessionService = class {
|
|
|
1847
1989
|
username,
|
|
1848
1990
|
realm: name
|
|
1849
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
|
+
});
|
|
1850
2001
|
throw new InvalidCredentialsError();
|
|
1851
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
|
+
});
|
|
1852
2014
|
return user;
|
|
1853
2015
|
} catch (error) {
|
|
1854
2016
|
if (error instanceof InvalidCredentialsError) throw error;
|
|
@@ -1899,6 +2061,14 @@ var SessionService = class {
|
|
|
1899
2061
|
sessionId: session.id,
|
|
1900
2062
|
userId: session.userId
|
|
1901
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
|
+
});
|
|
1902
2072
|
return {
|
|
1903
2073
|
user,
|
|
1904
2074
|
expiresIn: expiresAt.unix() - now.unix(),
|
|
@@ -1907,8 +2077,18 @@ var SessionService = class {
|
|
|
1907
2077
|
}
|
|
1908
2078
|
async deleteSession(refreshToken, userRealmName) {
|
|
1909
2079
|
this.log.trace("Deleting session");
|
|
2080
|
+
const session = await this.sessions(userRealmName).findOne({ where: { refreshToken: { eq: refreshToken } } }).catch(() => void 0);
|
|
1910
2081
|
await this.sessions(userRealmName).deleteOne({ refreshToken });
|
|
1911
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
|
+
}
|
|
1912
2092
|
}
|
|
1913
2093
|
async link(provider, profile, userRealmName) {
|
|
1914
2094
|
this.log.trace("Linking OAuth2 profile", {
|
|
@@ -1929,7 +2109,19 @@ var SessionService = class {
|
|
|
1929
2109
|
identityId: identity.id,
|
|
1930
2110
|
userId: identity.userId
|
|
1931
2111
|
});
|
|
1932
|
-
|
|
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;
|
|
1933
2125
|
}
|
|
1934
2126
|
if (!profile.email) {
|
|
1935
2127
|
this.log.debug("OAuth2 profile has no email, returning profile as-is", {
|
|
@@ -1954,6 +2146,18 @@ var SessionService = class {
|
|
|
1954
2146
|
providerUserId: profile.sub,
|
|
1955
2147
|
userId: existing.id
|
|
1956
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
|
+
});
|
|
1957
2161
|
return existing;
|
|
1958
2162
|
}
|
|
1959
2163
|
const user = await users$1.create({
|
|
@@ -1990,6 +2194,31 @@ var SessionService = class {
|
|
|
1990
2194
|
email: user.email,
|
|
1991
2195
|
username: user.username
|
|
1992
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
|
+
});
|
|
1993
2222
|
return user;
|
|
1994
2223
|
}
|
|
1995
2224
|
};
|
|
@@ -2015,10 +2244,13 @@ const $userRealm = (options = {}) => {
|
|
|
2015
2244
|
const securityProvider = alepha.inject(SecurityProvider);
|
|
2016
2245
|
const userRealmProvider = alepha.inject(UserRealmProvider);
|
|
2017
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;
|
|
2018
2251
|
const userRealm = userRealmProvider.register(name, options);
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
if (options.modules?.jobs) alepha.with(AlephaApiJobs);
|
|
2252
|
+
alepha.with(AlephaApiFiles);
|
|
2253
|
+
alepha.with(AlephaApiAudits);
|
|
2022
2254
|
const realm = $realm({
|
|
2023
2255
|
...options.realm,
|
|
2024
2256
|
name,
|
|
@@ -2141,6 +2373,8 @@ const AlephaApiUsers = $module({
|
|
|
2141
2373
|
services: [
|
|
2142
2374
|
AlephaApiVerification,
|
|
2143
2375
|
AlephaApiNotifications,
|
|
2376
|
+
AlephaServerHelmet,
|
|
2377
|
+
AlephaServerCompress,
|
|
2144
2378
|
AlephaEmail,
|
|
2145
2379
|
UserRealmProvider,
|
|
2146
2380
|
SessionService,
|
|
@@ -2150,13 +2384,14 @@ const AlephaApiUsers = $module({
|
|
|
2150
2384
|
UserService,
|
|
2151
2385
|
IdentityService,
|
|
2152
2386
|
UserController,
|
|
2153
|
-
|
|
2154
|
-
|
|
2387
|
+
AdminUserController,
|
|
2388
|
+
AdminSessionController,
|
|
2389
|
+
AdminIdentityController,
|
|
2155
2390
|
UserRealmController,
|
|
2156
2391
|
UserNotifications
|
|
2157
2392
|
]
|
|
2158
2393
|
});
|
|
2159
2394
|
|
|
2160
2395
|
//#endregion
|
|
2161
|
-
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 };
|
|
2162
2397
|
//# sourceMappingURL=index.js.map
|