alepha 0.20.5 → 0.20.7
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/AGENTS.md +0 -1
- package/CLAUDE.md +0 -1
- package/assets/agents-template.md +0 -1
- package/dist/api/audits/index.browser.js +1 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +701 -654
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +24 -1
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +1 -0
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +193 -166
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +52 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +40 -14
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +639 -333
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +495 -162
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +222 -188
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +54 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +265 -236
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +55 -13
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/organizations/index.d.ts +100 -97
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/organizations/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +332 -314
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +37 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +431 -376
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +202 -87
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/subscriptions/index.d.ts +1695 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1919 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +1001 -844
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +237 -28
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +123 -122
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +21 -2
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +47 -0
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +24 -0
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +134 -7
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +181 -15
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +181 -15
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +156 -0
- package/dist/cache/database/index.d.ts.map +1 -0
- package/dist/cache/database/index.js +266 -0
- package/dist/cache/database/index.js.map +1 -0
- package/dist/cache/redis/index.d.ts +3 -2
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +142 -128
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +160 -13
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +3 -2
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +346 -290
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +106 -7
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +12 -11
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts +6 -5
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +119 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.browser.js.map +1 -1
- package/dist/crypto/index.d.ts +3 -2
- package/dist/crypto/index.d.ts.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/brevo/index.js.map +1 -1
- package/dist/email/core/index.d.ts +3 -2
- package/dist/email/core/index.d.ts.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.d.ts +7 -6
- package/dist/email/smtp/index.d.ts.map +1 -1
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +5 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.d.ts +10 -9
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +9 -8
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +9 -3
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +31 -10
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +33 -14
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +31 -10
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +6 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +5 -4
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/core/index.js.map +1 -1
- package/dist/queue/core/index.workerd.js.map +1 -1
- package/dist/queue/redis/index.d.ts +3 -2
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +5 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +8 -4
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +2 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +206 -205
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +11 -11
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/redis/index.bun.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 +25 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +12 -0
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js +29 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +82 -35
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +56 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +163 -158
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -4
- package/dist/server/auth/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 +35 -34
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +7 -6
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/etag/index.js.map +1 -1
- package/dist/server/health/index.d.ts +16 -15
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +51 -50
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +6 -5
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +2 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/topic/redis/index.d.ts +3 -2
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/topic/redis/index.js.map +1 -1
- package/package.json +33 -39
- package/src/api/audits/controllers/AdminAuditController.ts +29 -0
- package/src/api/audits/entities/audits.ts +1 -0
- package/src/api/files/controllers/FileController.ts +24 -0
- package/src/api/files/entities/files.ts +1 -0
- package/src/api/files/services/FileService.ts +41 -0
- package/src/api/jobs/__tests__/$job.spec.ts +501 -24
- package/src/api/jobs/entities/jobExecutionEntity.ts +4 -3
- package/src/api/jobs/index.ts +47 -10
- package/src/api/jobs/primitives/$job.ts +22 -9
- package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
- package/src/api/jobs/providers/JobDispatcher.ts +49 -0
- package/src/api/jobs/providers/JobProvider.ts +385 -147
- package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
- package/src/api/jobs/schemas/jobConfigAtom.ts +9 -3
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
- package/src/api/jobs/services/JobService.ts +21 -11
- package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
- package/src/api/keys/entities/apiKeyEntity.ts +1 -0
- package/src/api/keys/services/ApiKeyService.ts +42 -0
- package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
- package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
- package/src/api/notifications/index.ts +13 -3
- package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
- package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
- package/src/api/parameters/services/ParameterProvider.ts +18 -0
- package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
- package/src/api/payments/index.ts +3 -0
- package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
- package/src/api/payments/providers/PaymentProvider.ts +25 -9
- package/src/api/payments/services/PaymentService.ts +3 -0
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +133 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/api/users/__tests__/Registration-emailMode.spec.ts +203 -0
- package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
- package/src/api/users/controllers/AdminSessionController.ts +29 -0
- package/src/api/users/controllers/AdminUserController.ts +32 -0
- package/src/api/users/index.ts +3 -0
- package/src/api/users/services/CredentialService.ts +5 -0
- package/src/api/users/services/RegistrationService.ts +49 -1
- package/src/api/users/services/SessionCrudService.ts +16 -0
- package/src/api/users/services/SessionService.ts +17 -59
- package/src/api/users/services/UsernameSlugger.ts +195 -0
- package/src/bucket/primitives/$bucket.ts +21 -0
- package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
- package/src/bucket/providers/FileStorageProvider.ts +9 -0
- package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
- package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
- package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
- package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
- package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
- package/src/cache/core/index.ts +16 -0
- package/src/cache/core/primitives/$cache.ts +367 -24
- package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
- package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
- package/src/cache/database/entities/cacheEntries.ts +55 -0
- package/src/cache/database/index.ts +36 -0
- package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
- package/src/cli/core/services/ProjectScaffolder.ts +0 -2
- package/src/cli/core/tasks/BuildCloudflareTask.ts +33 -3
- package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
- package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
- package/src/cli/core/templates/agentMd.ts +39 -4
- package/src/cli/core/templates/biomeJson.ts +25 -1
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
- package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
- package/src/cli/platform/atoms/platformOptions.ts +13 -0
- package/src/cli/platform/commands/platform.ts +7 -1
- package/src/cli/platform/schemas/platform.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +61 -0
- package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
- package/src/core/__tests__/$module.spec.ts +2 -2
- package/src/core/primitives/$module.ts +4 -4
- package/src/mcp/providers/McpServerProvider.ts +1 -1
- package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
- package/src/orm/core/schemas/insertSchema.ts +10 -2
- package/src/orm/core/services/Repository.ts +27 -7
- package/src/react/form/hooks/useFormState.ts +8 -1
- package/src/react/form/index.ts +10 -1
- package/src/react/form/services/FormModel.ts +9 -3
- package/src/scheduler/index.ts +14 -0
- package/src/scheduler/providers/CronProvider.ts +13 -0
- package/src/security/atoms/currentTenantAtom.ts +34 -0
- package/src/security/index.browser.ts +1 -0
- package/src/security/index.ts +12 -1
- package/src/security/primitives/$issuer.ts +17 -1
- package/src/security/providers/SecurityProvider.ts +37 -0
- package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
- package/tsconfig.base.json +2 -1
- package/dist/react/websocket/index.d.ts +0 -117
- package/dist/react/websocket/index.d.ts.map +0 -1
- package/dist/react/websocket/index.js +0 -108
- package/dist/react/websocket/index.js.map +0 -1
- package/dist/websocket/index.browser.js +0 -844
- package/dist/websocket/index.browser.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -876
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -1175
- package/dist/websocket/index.js.map +0 -1
- package/src/react/websocket/hooks/useRoom.tsx +0 -251
- package/src/react/websocket/index.ts +0 -7
- package/src/websocket/__tests__/$channel.spec.ts +0 -30
- package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
- package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
- package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
- package/src/websocket/errors/WebSocketError.ts +0 -34
- package/src/websocket/index.browser.ts +0 -25
- package/src/websocket/index.shared.ts +0 -8
- package/src/websocket/index.ts +0 -85
- package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
- package/src/websocket/primitives/$channel.ts +0 -131
- package/src/websocket/primitives/$websocket.ts +0 -107
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
- package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
- package/src/websocket/services/RoomManager.ts +0 -160
- package/src/websocket/services/WebSocketClient.ts +0 -642
- package/src/websocket/services/WebSocketTopicService.ts +0 -108
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { $inject } from "alepha";
|
|
2
|
+
import { CacheProvider } from "alepha/cache";
|
|
3
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
4
|
+
import { SubscriptionService } from "./SubscriptionService.ts";
|
|
5
|
+
|
|
6
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The result of a usage check or increment operation.
|
|
10
|
+
*/
|
|
11
|
+
export interface UsageResult {
|
|
12
|
+
/**
|
|
13
|
+
* Whether the operation is allowed given the current usage and limit.
|
|
14
|
+
*/
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Current usage count for the period.
|
|
19
|
+
*/
|
|
20
|
+
current: number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The plan limit for the resource. -1 means unlimited.
|
|
24
|
+
*/
|
|
25
|
+
limit: number;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Remaining capacity. -1 means unlimited.
|
|
29
|
+
*/
|
|
30
|
+
remaining: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tracks and enforces per-organization resource usage limits.
|
|
37
|
+
*
|
|
38
|
+
* Usage counters are keyed by `organizationId:resource:YYYY-MM` and stored in the cache.
|
|
39
|
+
* Limits are resolved from the organization's current subscription plan.
|
|
40
|
+
*/
|
|
41
|
+
export class UsageService {
|
|
42
|
+
protected readonly cache = $inject(CacheProvider);
|
|
43
|
+
protected readonly dateTime = $inject(DateTimeProvider);
|
|
44
|
+
protected readonly subscriptionService = $inject(SubscriptionService);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Increment a resource counter for the current period and return the usage result.
|
|
48
|
+
*
|
|
49
|
+
* @param organizationId The organization to track usage for.
|
|
50
|
+
* @param resource The resource identifier (e.g., "api_calls", "seats").
|
|
51
|
+
* @param amount Amount to increment by (default: 1).
|
|
52
|
+
*/
|
|
53
|
+
public async increment(
|
|
54
|
+
organizationId: string,
|
|
55
|
+
resource: string,
|
|
56
|
+
amount = 1,
|
|
57
|
+
): Promise<UsageResult> {
|
|
58
|
+
const limit = await this.subscriptionService.limit(
|
|
59
|
+
organizationId,
|
|
60
|
+
resource,
|
|
61
|
+
);
|
|
62
|
+
const key = this.buildKey(organizationId, resource);
|
|
63
|
+
const current = await this.cache.incr("subscriptions:usage", key, amount);
|
|
64
|
+
const allowed = limit === -1 || current <= limit;
|
|
65
|
+
return {
|
|
66
|
+
allowed,
|
|
67
|
+
current,
|
|
68
|
+
limit,
|
|
69
|
+
remaining: limit === -1 ? -1 : Math.max(0, limit - current),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the current usage for a resource without incrementing.
|
|
75
|
+
*
|
|
76
|
+
* @param organizationId The organization to query usage for.
|
|
77
|
+
* @param resource The resource identifier.
|
|
78
|
+
*/
|
|
79
|
+
public async getUsage(
|
|
80
|
+
organizationId: string,
|
|
81
|
+
resource: string,
|
|
82
|
+
): Promise<UsageResult> {
|
|
83
|
+
const limit = await this.subscriptionService.limit(
|
|
84
|
+
organizationId,
|
|
85
|
+
resource,
|
|
86
|
+
);
|
|
87
|
+
const key = this.buildKey(organizationId, resource);
|
|
88
|
+
const current = await this.cache.incr("subscriptions:usage", key, 0);
|
|
89
|
+
return {
|
|
90
|
+
allowed: limit === -1 || current <= limit,
|
|
91
|
+
current,
|
|
92
|
+
limit,
|
|
93
|
+
remaining: limit === -1 ? -1 : Math.max(0, limit - current),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reset all usage counters for an organization.
|
|
99
|
+
*
|
|
100
|
+
* Used at the start of a new billing period.
|
|
101
|
+
*
|
|
102
|
+
* @param organizationId The organization whose counters to reset.
|
|
103
|
+
*/
|
|
104
|
+
public async resetForPeriod(organizationId: string): Promise<void> {
|
|
105
|
+
const pattern = this.buildKey(organizationId, "*");
|
|
106
|
+
await this.cache.invalidateKeys("subscriptions:usage", [pattern]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the cache key for a usage counter.
|
|
111
|
+
*
|
|
112
|
+
* Format: `organizationId:resource:YYYY-MM`
|
|
113
|
+
*/
|
|
114
|
+
protected buildKey(organizationId: string, resource: string): string {
|
|
115
|
+
const period = this.dateTime.now().format("YYYY-MM");
|
|
116
|
+
return `${organizationId}:${resource}:${period}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { AlephaApiVerification } from "alepha/api/verifications";
|
|
3
|
+
import { AlephaEmail, MemoryEmailProvider } from "alepha/email";
|
|
4
|
+
import { AlephaOrmPostgres } from "alepha/orm/postgres";
|
|
5
|
+
import { AlephaSecurity } from "alepha/security";
|
|
6
|
+
import { BadRequestError } from "alepha/server";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
AlephaApiUsers,
|
|
10
|
+
RealmProvider,
|
|
11
|
+
RegistrationService,
|
|
12
|
+
UserNotifications,
|
|
13
|
+
UserService,
|
|
14
|
+
} from "../index.ts";
|
|
15
|
+
|
|
16
|
+
const setup = async (
|
|
17
|
+
realmName: string,
|
|
18
|
+
realmSettings?: Record<string, unknown>,
|
|
19
|
+
) => {
|
|
20
|
+
const alepha = Alepha.create();
|
|
21
|
+
alepha.with(AlephaOrmPostgres);
|
|
22
|
+
alepha.with(AlephaSecurity);
|
|
23
|
+
alepha.with(AlephaEmail);
|
|
24
|
+
alepha.with(AlephaApiVerification);
|
|
25
|
+
alepha.with(AlephaApiUsers);
|
|
26
|
+
alepha.with(UserNotifications);
|
|
27
|
+
await alepha.start();
|
|
28
|
+
|
|
29
|
+
const realmProvider = alepha.inject(RealmProvider);
|
|
30
|
+
if (realmSettings) {
|
|
31
|
+
realmProvider.register(realmName, {
|
|
32
|
+
settings: realmSettings as never,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Wipe between cases.
|
|
37
|
+
await realmProvider.userRepository(realmName).deleteMany({});
|
|
38
|
+
alepha.inject(MemoryEmailProvider).records = [];
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
alepha,
|
|
42
|
+
registrationService: alepha.inject(RegistrationService),
|
|
43
|
+
userService: alepha.inject(UserService),
|
|
44
|
+
realmProvider,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
describe("RegistrationService — username: 'email' mode", () => {
|
|
51
|
+
it("derives the username from the email and ignores any client-sent value", async () => {
|
|
52
|
+
const { registrationService, userService } = await setup("email-mode-1", {
|
|
53
|
+
username: "email",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const intent = await registrationService.createRegistrationIntent(
|
|
57
|
+
{
|
|
58
|
+
email: "ni.foures+testkv@gmail.com",
|
|
59
|
+
password: "SecurePassword123!",
|
|
60
|
+
// The client sneaks a custom username into the request — server
|
|
61
|
+
// must drop it on the floor and use the slugger.
|
|
62
|
+
username: "i-am-the-admin" as never,
|
|
63
|
+
},
|
|
64
|
+
"email-mode-1",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const user = await registrationService.completeRegistration({
|
|
68
|
+
intentId: intent.intentId,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Slug rule: gmail "+suffix" preserved, dots → "-".
|
|
72
|
+
expect(user.username).toBe("ni-foures-testkv");
|
|
73
|
+
|
|
74
|
+
const reloaded = await userService
|
|
75
|
+
.users("email-mode-1")
|
|
76
|
+
.findOne({ where: { email: { eq: "ni.foures+testkv@gmail.com" } } });
|
|
77
|
+
expect(reloaded?.username).toBe("ni-foures-testkv");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("appends a 4-char random suffix on collision instead of failing", async () => {
|
|
81
|
+
const { registrationService, userService } = await setup("email-mode-2", {
|
|
82
|
+
username: "email",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// First user takes the slug.
|
|
86
|
+
await userService.users("email-mode-2").create({
|
|
87
|
+
realm: "email-mode-2",
|
|
88
|
+
username: "alice",
|
|
89
|
+
email: "preexisting@example.com",
|
|
90
|
+
roles: ["user"],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const intent = await registrationService.createRegistrationIntent(
|
|
94
|
+
{
|
|
95
|
+
email: "alice@example.com",
|
|
96
|
+
password: "SecurePassword123!",
|
|
97
|
+
},
|
|
98
|
+
"email-mode-2",
|
|
99
|
+
);
|
|
100
|
+
const created = await registrationService.completeRegistration({
|
|
101
|
+
intentId: intent.intentId,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(created.username).toMatch(/^alice-[a-z0-9]{4}$/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("rejects email-mode when no email is provided", async () => {
|
|
108
|
+
const { registrationService } = await setup("email-mode-3", {
|
|
109
|
+
username: "email",
|
|
110
|
+
email: "optional",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await expect(
|
|
114
|
+
registrationService.createRegistrationIntent(
|
|
115
|
+
{
|
|
116
|
+
password: "SecurePassword123!",
|
|
117
|
+
},
|
|
118
|
+
"email-mode-3",
|
|
119
|
+
),
|
|
120
|
+
).rejects.toThrowError(BadRequestError);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("blocklist applies even though the client never sees the field", async () => {
|
|
124
|
+
const { registrationService } = await setup("email-mode-blocklist", {
|
|
125
|
+
username: "email",
|
|
126
|
+
usernameBlocklist: ["admin"],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const intent = await registrationService.createRegistrationIntent(
|
|
130
|
+
{
|
|
131
|
+
email: "admin@example.com",
|
|
132
|
+
password: "SecurePassword123!",
|
|
133
|
+
},
|
|
134
|
+
"email-mode-blocklist",
|
|
135
|
+
);
|
|
136
|
+
const user = await registrationService.completeRegistration({
|
|
137
|
+
intentId: intent.intentId,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Slug "admin" is blocked → falls through to the suffix path.
|
|
141
|
+
expect(user.username).not.toBe("admin");
|
|
142
|
+
expect(user.username).toMatch(/^admin-[a-z0-9]{4}$/);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
describe("RegistrationService — blocklist applies in 'required' mode too", () => {
|
|
149
|
+
it("rejects a manual username that hits the blocklist", async () => {
|
|
150
|
+
const { registrationService } = await setup("manual-mode", {
|
|
151
|
+
username: "required",
|
|
152
|
+
usernameBlocklist: ["admin"],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await expect(
|
|
156
|
+
registrationService.createRegistrationIntent(
|
|
157
|
+
{
|
|
158
|
+
username: "admin",
|
|
159
|
+
email: "someone@example.com",
|
|
160
|
+
password: "SecurePassword123!",
|
|
161
|
+
},
|
|
162
|
+
"manual-mode",
|
|
163
|
+
),
|
|
164
|
+
).rejects.toThrowError(BadRequestError);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("blocklist match is case-insensitive in manual mode", async () => {
|
|
168
|
+
const { registrationService } = await setup("manual-case", {
|
|
169
|
+
username: "required",
|
|
170
|
+
usernameBlocklist: ["Admin"],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
registrationService.createRegistrationIntent(
|
|
175
|
+
{
|
|
176
|
+
username: "ADMIN",
|
|
177
|
+
email: "someone@example.com",
|
|
178
|
+
password: "SecurePassword123!",
|
|
179
|
+
},
|
|
180
|
+
"manual-case",
|
|
181
|
+
),
|
|
182
|
+
).rejects.toThrowError(BadRequestError);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("default (empty) blocklist does not reject special names", async () => {
|
|
186
|
+
const { registrationService } = await setup("manual-empty", {
|
|
187
|
+
username: "required",
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const intent = await registrationService.createRegistrationIntent(
|
|
191
|
+
{
|
|
192
|
+
username: "admin",
|
|
193
|
+
email: "admin-empty@example.com",
|
|
194
|
+
password: "SecurePassword123!",
|
|
195
|
+
},
|
|
196
|
+
"manual-empty",
|
|
197
|
+
);
|
|
198
|
+
const created = await registrationService.completeRegistration({
|
|
199
|
+
intentId: intent.intentId,
|
|
200
|
+
});
|
|
201
|
+
expect(created.username).toBe("admin");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { AlephaOrmPostgres } from "alepha/orm/postgres";
|
|
3
|
+
import { AlephaSecurity } from "alepha/security";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { realmAuthSettingsAtom } from "../atoms/realmAuthSettingsAtom.ts";
|
|
6
|
+
import { AlephaApiUsers, RealmProvider, UsernameSlugger } from "../index.ts";
|
|
7
|
+
|
|
8
|
+
const setup = async (realmSettings?: Record<string, unknown>) => {
|
|
9
|
+
const alepha = Alepha.create();
|
|
10
|
+
alepha.with(AlephaOrmPostgres);
|
|
11
|
+
alepha.with(AlephaSecurity);
|
|
12
|
+
alepha.with(AlephaApiUsers);
|
|
13
|
+
|
|
14
|
+
await alepha.start();
|
|
15
|
+
|
|
16
|
+
if (realmSettings) {
|
|
17
|
+
alepha.inject(RealmProvider).register("default", {
|
|
18
|
+
settings: realmSettings as never,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Wipe users table between cases so collision tests don't interfere.
|
|
23
|
+
const users = alepha.inject(RealmProvider).userRepository();
|
|
24
|
+
await users.deleteMany({});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
alepha,
|
|
28
|
+
slugger: alepha.inject(UsernameSlugger),
|
|
29
|
+
users,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
describe("UsernameSlugger.slug — pure rule", () => {
|
|
36
|
+
it("strips the domain and the gmail '+suffix' is kept verbatim", async () => {
|
|
37
|
+
const { slugger } = await setup();
|
|
38
|
+
expect(slugger.slug("ni.foures+testkv@gmail.com")).toBe("ni-foures-testkv");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("collapses runs of non-alphanumeric chars and trims edges", async () => {
|
|
42
|
+
const { slugger } = await setup();
|
|
43
|
+
expect(slugger.slug("john...doe@example.com")).toBe("john-doe");
|
|
44
|
+
expect(slugger.slug("---weird---name---@x.io")).toBe("weird-name");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("lowercases the result", async () => {
|
|
48
|
+
const { slugger } = await setup();
|
|
49
|
+
expect(slugger.slug("Alice.SMITH@Example.com")).toBe("alice-smith");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("drops every non-[a-z0-9] char (no NFD normalize)", async () => {
|
|
53
|
+
// `é` is dropped, so `josé` becomes `jos` — and then padded to MIN_LENGTH.
|
|
54
|
+
const { slugger } = await setup();
|
|
55
|
+
const result = slugger.slug("josé@example.com");
|
|
56
|
+
expect(result.startsWith("jos")).toBe(true);
|
|
57
|
+
expect(result.length).toBeGreaterThanOrEqual(UsernameSlugger.MIN_LENGTH);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("pads short slugs with random alphanumerics", async () => {
|
|
61
|
+
const { slugger } = await setup();
|
|
62
|
+
const result = slugger.slug("a@example.com");
|
|
63
|
+
expect(result.startsWith("a")).toBe(true);
|
|
64
|
+
expect(result.length).toBeGreaterThanOrEqual(UsernameSlugger.MIN_LENGTH);
|
|
65
|
+
expect(result).toMatch(/^[a-z0-9-]+$/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("falls back to the EMPTY_LOCAL_FALLBACK when the local part has no [a-z0-9]", async () => {
|
|
69
|
+
const { slugger } = await setup();
|
|
70
|
+
expect(slugger.slug("é@example.com")).toBe(
|
|
71
|
+
UsernameSlugger.EMPTY_LOCAL_FALLBACK,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("clamps to MAX_LENGTH", async () => {
|
|
76
|
+
const { slugger } = await setup();
|
|
77
|
+
const long = `${"a".repeat(60)}@example.com`;
|
|
78
|
+
expect(slugger.slug(long).length).toBe(UsernameSlugger.MAX_LENGTH);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("treats null/empty input by returning the fallback", async () => {
|
|
82
|
+
const { slugger } = await setup();
|
|
83
|
+
expect(slugger.slug(null)).toBe(UsernameSlugger.EMPTY_LOCAL_FALLBACK);
|
|
84
|
+
expect(slugger.slug("")).toBe(UsernameSlugger.EMPTY_LOCAL_FALLBACK);
|
|
85
|
+
expect(slugger.slug(undefined)).toBe(UsernameSlugger.EMPTY_LOCAL_FALLBACK);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
describe("UsernameSlugger.pickAvailable — DB-backed availability + retry", () => {
|
|
92
|
+
it("returns the base when nothing collides", async () => {
|
|
93
|
+
const { slugger } = await setup();
|
|
94
|
+
const picked = await slugger.pickAvailable("default", "alice");
|
|
95
|
+
expect(picked).toBe("alice");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("appends a 4-char random suffix when the base is taken", async () => {
|
|
99
|
+
const { slugger, users } = await setup();
|
|
100
|
+
await users.create({ realm: "default", username: "alice", email: "a@x" });
|
|
101
|
+
|
|
102
|
+
const picked = await slugger.pickAvailable("default", "alice");
|
|
103
|
+
expect(picked).toMatch(/^alice-[a-z0-9]{4}$/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("treats blocklisted candidates as collisions and falls through to the suffix path", async () => {
|
|
107
|
+
const { slugger } = await setup({ usernameBlocklist: ["admin", "root"] });
|
|
108
|
+
|
|
109
|
+
const picked = await slugger.pickAvailable("default", "admin");
|
|
110
|
+
expect(picked).toMatch(/^admin-[a-z0-9]{4}$/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("trims the base before adding the suffix when MAX_LENGTH would be exceeded", async () => {
|
|
114
|
+
const { slugger, users } = await setup();
|
|
115
|
+
const long = "a".repeat(UsernameSlugger.MAX_LENGTH);
|
|
116
|
+
await users.create({ realm: "default", username: long, email: "a@x" });
|
|
117
|
+
|
|
118
|
+
const picked = await slugger.pickAvailable("default", long);
|
|
119
|
+
expect(picked.length).toBeLessThanOrEqual(UsernameSlugger.MAX_LENGTH);
|
|
120
|
+
expect(picked).toMatch(/-[a-z0-9]{4}$/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("blocklist match is case-insensitive", async () => {
|
|
124
|
+
const { slugger } = await setup({ usernameBlocklist: ["Admin"] });
|
|
125
|
+
expect(await slugger.isBlocked("default", "admin")).toBe(true);
|
|
126
|
+
expect(await slugger.isBlocked("default", "ADMIN")).toBe(true);
|
|
127
|
+
expect(await slugger.isBlocked("default", "user")).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("default blocklist is empty — no name is implicitly reserved", async () => {
|
|
131
|
+
const { alepha, slugger } = await setup();
|
|
132
|
+
const settings = alepha.get(realmAuthSettingsAtom);
|
|
133
|
+
expect(settings.usernameBlocklist).toEqual([]);
|
|
134
|
+
|
|
135
|
+
const picked = await slugger.pickAvailable("default", "admin");
|
|
136
|
+
expect(picked).toBe("admin");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -14,6 +14,31 @@ const fieldRequirement = (description: string) =>
|
|
|
14
14
|
description,
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Username-specific field requirement, extending {@link FieldRequirement}
|
|
19
|
+
* with an additional auto-derivation mode.
|
|
20
|
+
*
|
|
21
|
+
* - `"none"` / `"optional"` / `"required"`: same as {@link FieldRequirement}.
|
|
22
|
+
* - `"email"`: Field is hidden in the registration UI and the value is
|
|
23
|
+
* auto-derived from the user's email at signup. Same handling on
|
|
24
|
+
* credentials and OAuth flows. See `UsernameSlugger` for the rule and
|
|
25
|
+
* collision behavior.
|
|
26
|
+
*/
|
|
27
|
+
export type UsernameFieldRequirement = FieldRequirement | "email";
|
|
28
|
+
|
|
29
|
+
const usernameFieldRequirement = (description: string) =>
|
|
30
|
+
t.union(
|
|
31
|
+
[
|
|
32
|
+
t.const("none"),
|
|
33
|
+
t.const("optional"),
|
|
34
|
+
t.const("required"),
|
|
35
|
+
t.const("email"),
|
|
36
|
+
],
|
|
37
|
+
{
|
|
38
|
+
description,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
17
42
|
export const realmAuthSettingsAtom = $atom({
|
|
18
43
|
name: "alepha.api.users.realmAuthSettings",
|
|
19
44
|
schema: t.object({
|
|
@@ -42,11 +67,20 @@ export const realmAuthSettingsAtom = $atom({
|
|
|
42
67
|
email: fieldRequirement(
|
|
43
68
|
"Email address field requirement for user accounts",
|
|
44
69
|
),
|
|
45
|
-
username:
|
|
70
|
+
username: usernameFieldRequirement(
|
|
71
|
+
"Username field requirement for user accounts",
|
|
72
|
+
),
|
|
46
73
|
usernameRegExp: t.string({
|
|
47
74
|
description:
|
|
48
75
|
"Regular expression that usernames must match (if username is enabled)",
|
|
49
76
|
}),
|
|
77
|
+
usernameBlocklist: t.array(t.text(), {
|
|
78
|
+
description:
|
|
79
|
+
"Usernames that the slugger / manual registration must reject. " +
|
|
80
|
+
"Default empty so apps can register `admin`/`root`/`me`/etc. if " +
|
|
81
|
+
"they want; populate it explicitly for handles you want to keep " +
|
|
82
|
+
"off-limits.",
|
|
83
|
+
}),
|
|
50
84
|
phoneNumber: fieldRequirement(
|
|
51
85
|
"Phone number field requirement for user accounts",
|
|
52
86
|
),
|
|
@@ -138,8 +172,12 @@ export const realmAuthSettingsAtom = $atom({
|
|
|
138
172
|
// for a fresh hello world setup, we accept registration and email login
|
|
139
173
|
registrationAllowed: true,
|
|
140
174
|
email: "required" as FieldRequirement,
|
|
141
|
-
username: "none" as
|
|
142
|
-
|
|
175
|
+
username: "none" as UsernameFieldRequirement,
|
|
176
|
+
// Allow hyphens by default so the UsernameSlugger output (`ni-foures-testkv`)
|
|
177
|
+
// matches without app overrides. Existing `[a-zA-Z0-9_]` usernames remain
|
|
178
|
+
// valid because the new pattern is a strict superset.
|
|
179
|
+
usernameRegExp: "^[a-zA-Z0-9_-]{3,30}$",
|
|
180
|
+
usernameBlocklist: [] as string[],
|
|
143
181
|
phoneNumber: "none" as FieldRequirement,
|
|
144
182
|
verifyEmailRequired: false,
|
|
145
183
|
verifyPhoneRequired: false,
|
|
@@ -74,4 +74,33 @@ export class AdminSessionController {
|
|
|
74
74
|
return { ok: true, id: params.id };
|
|
75
75
|
},
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Delete many sessions in one repository call.
|
|
80
|
+
*/
|
|
81
|
+
public readonly deleteSessions = $action({
|
|
82
|
+
method: "POST",
|
|
83
|
+
path: `${this.url}/delete`,
|
|
84
|
+
group: this.group,
|
|
85
|
+
use: [$secure({ permissions: ["admin:session:delete"] })],
|
|
86
|
+
description: "Delete many sessions",
|
|
87
|
+
schema: {
|
|
88
|
+
query: t.object({
|
|
89
|
+
userRealmName: t.optional(t.string()),
|
|
90
|
+
}),
|
|
91
|
+
body: t.object({
|
|
92
|
+
ids: t.array(t.uuid(), { minItems: 1, maxItems: 1000 }),
|
|
93
|
+
}),
|
|
94
|
+
response: t.object({
|
|
95
|
+
deleted: t.array(t.string()),
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
handler: async ({ body, query }) => {
|
|
99
|
+
const deleted = await this.sessionService.deleteSessions(
|
|
100
|
+
body.ids,
|
|
101
|
+
query.userRealmName,
|
|
102
|
+
);
|
|
103
|
+
return { deleted };
|
|
104
|
+
},
|
|
105
|
+
});
|
|
77
106
|
}
|
|
@@ -119,4 +119,36 @@ export class AdminUserController {
|
|
|
119
119
|
return { ok: true, id: params.id };
|
|
120
120
|
},
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Delete many users in one request. Each id is processed sequentially so
|
|
125
|
+
* cascades and side-effects run as if called one-by-one. Errors on a single
|
|
126
|
+
* id surface with that id in the response.
|
|
127
|
+
*/
|
|
128
|
+
public readonly deleteUsers = $action({
|
|
129
|
+
method: "POST",
|
|
130
|
+
path: `${this.url}/delete`,
|
|
131
|
+
group: this.group,
|
|
132
|
+
use: [$secure({ permissions: ["admin:user:delete"] })],
|
|
133
|
+
description: "Delete many users",
|
|
134
|
+
schema: {
|
|
135
|
+
query: t.object({
|
|
136
|
+
userRealmName: t.optional(t.string()),
|
|
137
|
+
}),
|
|
138
|
+
body: t.object({
|
|
139
|
+
ids: t.array(t.uuid(), { minItems: 1, maxItems: 1000 }),
|
|
140
|
+
}),
|
|
141
|
+
response: t.object({
|
|
142
|
+
deleted: t.array(t.uuid()),
|
|
143
|
+
}),
|
|
144
|
+
},
|
|
145
|
+
handler: async ({ body, query }) => {
|
|
146
|
+
const deleted: string[] = [];
|
|
147
|
+
for (const id of body.ids) {
|
|
148
|
+
await this.userService.deleteUser(id, query.userRealmName);
|
|
149
|
+
deleted.push(id);
|
|
150
|
+
}
|
|
151
|
+
return { deleted };
|
|
152
|
+
},
|
|
153
|
+
});
|
|
122
154
|
}
|
package/src/api/users/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { IdentityService } from "./services/IdentityService.ts";
|
|
|
14
14
|
import { RegistrationService } from "./services/RegistrationService.ts";
|
|
15
15
|
import { SessionCrudService } from "./services/SessionCrudService.ts";
|
|
16
16
|
import { SessionService } from "./services/SessionService.ts";
|
|
17
|
+
import { UsernameSlugger } from "./services/UsernameSlugger.ts";
|
|
17
18
|
import { UserService } from "./services/UserService.ts";
|
|
18
19
|
|
|
19
20
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -54,6 +55,7 @@ export * from "./services/IdentityService.ts";
|
|
|
54
55
|
export * from "./services/RegistrationService.ts";
|
|
55
56
|
export * from "./services/SessionCrudService.ts";
|
|
56
57
|
export * from "./services/SessionService.ts";
|
|
58
|
+
export * from "./services/UsernameSlugger.ts";
|
|
57
59
|
export * from "./services/UserService.ts";
|
|
58
60
|
|
|
59
61
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -82,6 +84,7 @@ export const AlephaApiUsers = $module({
|
|
|
82
84
|
CredentialService,
|
|
83
85
|
RegistrationService,
|
|
84
86
|
UserService,
|
|
87
|
+
UsernameSlugger,
|
|
85
88
|
IdentityService,
|
|
86
89
|
UserController,
|
|
87
90
|
AdminUserController,
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { $inject, Alepha } from "alepha";
|
|
3
3
|
import type { VerificationController } from "alepha/api/verifications";
|
|
4
4
|
import { $cache } from "alepha/cache";
|
|
5
|
+
import { DatabaseCacheProvider } from "alepha/cache/database";
|
|
5
6
|
import { DateTimeProvider } from "alepha/datetime";
|
|
6
7
|
import { $logger } from "alepha/logger";
|
|
7
8
|
import { CryptoProvider } from "alepha/security";
|
|
@@ -52,6 +53,10 @@ export class CredentialService {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
protected readonly intentCache = $cache<PasswordResetIntent>({
|
|
56
|
+
// Use the SQL-backed cache so phase-2 reads what phase-1 wrote with
|
|
57
|
+
// strong consistency, and so bare deployments don't need a distributed
|
|
58
|
+
// KV resource just to support password reset.
|
|
59
|
+
provider: DatabaseCacheProvider,
|
|
55
60
|
name: "api:users:password-reset-intents",
|
|
56
61
|
ttl: [INTENT_TTL_MINUTES, "minutes"],
|
|
57
62
|
});
|