alepha 0.19.3 → 0.19.5

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.
Files changed (289) hide show
  1. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/invitations/index.d.ts +790 -0
  3. package/dist/api/invitations/index.d.ts.map +1 -0
  4. package/dist/api/invitations/index.js +665 -0
  5. package/dist/api/invitations/index.js.map +1 -0
  6. package/dist/api/issues/index.d.ts +810 -0
  7. package/dist/api/issues/index.d.ts.map +1 -0
  8. package/dist/api/issues/index.js +447 -0
  9. package/dist/api/issues/index.js.map +1 -0
  10. package/dist/api/jobs/index.browser.js +8 -9
  11. package/dist/api/jobs/index.browser.js.map +1 -1
  12. package/dist/api/jobs/index.d.ts +99 -43
  13. package/dist/api/jobs/index.d.ts.map +1 -1
  14. package/dist/api/jobs/index.js +257 -40
  15. package/dist/api/jobs/index.js.map +1 -1
  16. package/dist/api/notifications/index.browser.js +0 -1
  17. package/dist/api/notifications/index.browser.js.map +1 -1
  18. package/dist/api/notifications/index.d.ts +3 -3
  19. package/dist/api/notifications/index.d.ts.map +1 -1
  20. package/dist/api/notifications/index.js +0 -1
  21. package/dist/api/notifications/index.js.map +1 -1
  22. package/dist/api/parameters/index.browser.js +112 -1
  23. package/dist/api/parameters/index.browser.js.map +1 -1
  24. package/dist/api/parameters/index.d.ts +90 -3
  25. package/dist/api/parameters/index.d.ts.map +1 -1
  26. package/dist/api/parameters/index.js +79 -12
  27. package/dist/api/parameters/index.js.map +1 -1
  28. package/dist/{billing → api/payments}/index.d.ts +67 -49
  29. package/dist/api/payments/index.d.ts.map +1 -0
  30. package/dist/{billing → api/payments}/index.js +108 -74
  31. package/dist/api/payments/index.js.map +1 -0
  32. package/dist/api/subscriptions/index.d.ts +1692 -0
  33. package/dist/api/subscriptions/index.d.ts.map +1 -0
  34. package/dist/api/subscriptions/index.js +1870 -0
  35. package/dist/api/subscriptions/index.js.map +1 -0
  36. package/dist/api/users/index.d.ts +24 -2
  37. package/dist/api/users/index.d.ts.map +1 -1
  38. package/dist/api/users/index.js +176 -36
  39. package/dist/api/users/index.js.map +1 -1
  40. package/dist/api/verifications/index.d.ts +13 -13
  41. package/dist/api/workflows/index.browser.js +246 -0
  42. package/dist/api/workflows/index.browser.js.map +1 -0
  43. package/dist/api/workflows/index.d.ts +1618 -0
  44. package/dist/api/workflows/index.d.ts.map +1 -0
  45. package/dist/api/workflows/index.js +1504 -0
  46. package/dist/api/workflows/index.js.map +1 -0
  47. package/dist/captcha/index.d.ts +142 -0
  48. package/dist/captcha/index.d.ts.map +1 -0
  49. package/dist/captcha/index.js +177 -0
  50. package/dist/captcha/index.js.map +1 -0
  51. package/dist/cli/core/index.d.ts +126 -30
  52. package/dist/cli/core/index.d.ts.map +1 -1
  53. package/dist/cli/core/index.js +106 -67
  54. package/dist/cli/core/index.js.map +1 -1
  55. package/dist/cli/platform/index.d.ts +84 -10
  56. package/dist/cli/platform/index.d.ts.map +1 -1
  57. package/dist/cli/platform/index.js +92 -4
  58. package/dist/cli/platform/index.js.map +1 -1
  59. package/dist/cli/vendor/index.d.ts +60 -10
  60. package/dist/cli/vendor/index.d.ts.map +1 -1
  61. package/dist/cli/vendor/index.js +177 -45
  62. package/dist/cli/vendor/index.js.map +1 -1
  63. package/dist/command/index.d.ts.map +1 -1
  64. package/dist/command/index.js +2 -3
  65. package/dist/command/index.js.map +1 -1
  66. package/dist/core/index.browser.js +21 -2
  67. package/dist/core/index.browser.js.map +1 -1
  68. package/dist/core/index.d.ts +33 -2
  69. package/dist/core/index.d.ts.map +1 -1
  70. package/dist/core/index.js +21 -2
  71. package/dist/core/index.js.map +1 -1
  72. package/dist/core/index.native.js +21 -2
  73. package/dist/core/index.native.js.map +1 -1
  74. package/dist/core/index.workerd.js +21 -2
  75. package/dist/core/index.workerd.js.map +1 -1
  76. package/dist/email/smtp/index.js +24 -8
  77. package/dist/email/smtp/index.js.map +1 -1
  78. package/dist/orm/core/index.browser.js +0 -18
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js +6 -23
  81. package/dist/orm/core/index.bun.js.map +1 -1
  82. package/dist/orm/core/index.d.ts +1 -13
  83. package/dist/orm/core/index.d.ts.map +1 -1
  84. package/dist/orm/core/index.js +6 -23
  85. package/dist/orm/core/index.js.map +1 -1
  86. package/dist/orm/postgres/index.bun.js +3 -3
  87. package/dist/orm/postgres/index.bun.js.map +1 -1
  88. package/dist/orm/postgres/index.d.ts.map +1 -1
  89. package/dist/orm/postgres/index.js +3 -3
  90. package/dist/orm/postgres/index.js.map +1 -1
  91. package/dist/react/i18n/index.d.ts +1 -0
  92. package/dist/react/i18n/index.d.ts.map +1 -1
  93. package/dist/react/i18n/index.js +8 -4
  94. package/dist/react/i18n/index.js.map +1 -1
  95. package/dist/react/router/index.browser.js +25 -3
  96. package/dist/react/router/index.browser.js.map +1 -1
  97. package/dist/react/router/index.d.ts +16 -1
  98. package/dist/react/router/index.d.ts.map +1 -1
  99. package/dist/react/router/index.js +25 -3
  100. package/dist/react/router/index.js.map +1 -1
  101. package/dist/security/index.d.ts +28 -0
  102. package/dist/security/index.d.ts.map +1 -1
  103. package/dist/security/index.js +28 -0
  104. package/dist/security/index.js.map +1 -1
  105. package/dist/server/auth/index.d.ts +145 -2
  106. package/dist/server/auth/index.d.ts.map +1 -1
  107. package/dist/server/auth/index.js +364 -63
  108. package/dist/server/auth/index.js.map +1 -1
  109. package/dist/server/cookies/index.d.ts.map +1 -1
  110. package/dist/server/cookies/index.js.map +1 -1
  111. package/dist/websocket/index.d.ts.map +1 -1
  112. package/dist/websocket/index.js.map +1 -1
  113. package/package.json +47 -20
  114. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  115. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  116. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  117. package/src/api/invitations/entities/invitations.ts +33 -0
  118. package/src/api/invitations/index.ts +65 -0
  119. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  120. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  121. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  122. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  123. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  124. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  125. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  126. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  127. package/src/api/invitations/services/InvitationService.ts +556 -0
  128. package/src/api/issues/__tests__/IssueService.spec.ts +263 -0
  129. package/src/api/issues/controllers/AdminIssueController.ts +149 -0
  130. package/src/api/issues/controllers/IssueController.ts +44 -0
  131. package/src/api/issues/entities/issues.ts +49 -0
  132. package/src/api/issues/index.ts +53 -0
  133. package/src/api/issues/schemas/createIssueSchema.ts +13 -0
  134. package/src/api/issues/schemas/issueConfigAtom.ts +13 -0
  135. package/src/api/issues/schemas/issueQuerySchema.ts +18 -0
  136. package/src/api/issues/schemas/issueResourceSchema.ts +6 -0
  137. package/src/api/issues/schemas/myIssueQuerySchema.ts +10 -0
  138. package/src/api/issues/schemas/updateIssueSchema.ts +13 -0
  139. package/src/api/issues/services/IssueService.ts +264 -0
  140. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  141. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  142. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  143. package/src/api/jobs/index.ts +0 -3
  144. package/src/api/jobs/primitives/$job.ts +22 -11
  145. package/src/api/jobs/providers/JobProvider.ts +229 -19
  146. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  147. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  148. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  149. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  150. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  151. package/src/api/jobs/services/JobService.ts +51 -12
  152. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  153. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  154. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  155. package/src/api/parameters/index.browser.ts +12 -0
  156. package/src/api/parameters/primitives/$parameter.ts +20 -3
  157. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  158. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  159. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  160. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  161. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  162. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  163. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  164. package/src/{billing → api/payments}/index.ts +31 -25
  165. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  166. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  167. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  168. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  169. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  170. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  171. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  172. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  173. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  174. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  175. package/src/api/subscriptions/index.ts +144 -0
  176. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  177. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  178. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  179. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  180. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  181. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  182. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  183. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  184. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  185. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  186. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  187. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  188. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  189. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  190. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  191. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  192. package/src/api/subscriptions/services/BillingService.ts +437 -0
  193. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  194. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  195. package/src/api/subscriptions/services/UsageService.ts +118 -0
  196. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  197. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  198. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  199. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  200. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  201. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  202. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  203. package/src/api/users/controllers/UserController.ts +3 -8
  204. package/src/api/users/notifications/UserNotifications.ts +23 -0
  205. package/src/api/users/primitives/$realm.ts +24 -0
  206. package/src/api/users/schemas/loginSchema.ts +1 -1
  207. package/src/api/users/services/CredentialService.ts +57 -7
  208. package/src/api/users/services/RegistrationService.ts +50 -11
  209. package/src/api/users/services/SessionService.ts +64 -9
  210. package/src/api/users/services/UserService.ts +21 -12
  211. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  212. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  213. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  214. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  215. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  216. package/src/api/workflows/index.browser.ts +22 -0
  217. package/src/api/workflows/index.ts +124 -0
  218. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  219. package/src/api/workflows/primitives/$workflow.ts +202 -0
  220. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  221. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  222. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  223. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  224. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  225. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  226. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  227. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  228. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  229. package/src/api/workflows/services/WorkflowService.ts +382 -0
  230. package/src/captcha/__tests__/MemoryCaptchaProvider.spec.ts +74 -0
  231. package/src/captcha/index.ts +33 -0
  232. package/src/captcha/providers/CaptchaProvider.ts +17 -0
  233. package/src/captcha/providers/MemoryCaptchaProvider.ts +65 -0
  234. package/src/captcha/providers/TurnstileCaptchaProvider.ts +125 -0
  235. package/src/cli/core/atoms/buildOptions.ts +57 -0
  236. package/src/cli/core/commands/build.ts +2 -0
  237. package/src/cli/core/providers/ViteDevServerProvider.ts +1 -1
  238. package/src/cli/core/services/ViteUtils.ts +5 -2
  239. package/src/cli/core/tasks/BuildClientTask.ts +3 -1
  240. package/src/cli/core/tasks/BuildCloudflareTask.ts +4 -0
  241. package/src/cli/core/tasks/BuildPwaTask.ts +81 -0
  242. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  243. package/src/cli/platform/adapters/CloudflareAdapter.ts +24 -0
  244. package/src/cli/platform/atoms/platformOptions.ts +19 -3
  245. package/src/cli/platform/hooks/PlatformHook.ts +51 -0
  246. package/src/cli/platform/index.ts +1 -0
  247. package/src/cli/platform/services/CloudflareApi.ts +22 -1
  248. package/src/cli/platform/services/PlatformOrchestrator.ts +67 -2
  249. package/src/cli/vendor/__tests__/VendorService.spec.ts +322 -178
  250. package/src/cli/vendor/commands/VendorCommand.ts +41 -38
  251. package/src/cli/vendor/services/VendorService.ts +234 -31
  252. package/src/command/__tests__/CliProvider.spec.ts +45 -0
  253. package/src/command/providers/CliProvider.ts +3 -4
  254. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  255. package/src/core/providers/SchemaValidator.ts +1 -1
  256. package/src/core/providers/TypeProvider.ts +46 -3
  257. package/src/orm/__tests__/enums.spec.ts +22 -29
  258. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  259. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  260. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  261. package/src/orm/core/services/Repository.ts +20 -6
  262. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  263. package/src/react/i18n/__tests__/I18nProvider.spec.ts +83 -0
  264. package/src/react/i18n/providers/I18nProvider.ts +12 -10
  265. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  266. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  267. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  268. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  269. package/src/security/primitives/$issuer.ts +3 -1
  270. package/src/security/primitives/$secure.ts +28 -0
  271. package/src/server/auth/index.ts +7 -0
  272. package/src/server/auth/primitives/$auth.ts +37 -3
  273. package/src/server/auth/primitives/$authApple.ts +114 -4
  274. package/src/server/auth/primitives/$authFacebook.ts +98 -0
  275. package/src/server/auth/primitives/$authFranceConnect.ts +105 -0
  276. package/src/server/auth/primitives/$authGithub.ts +22 -16
  277. package/src/server/auth/primitives/$authMicrosoft.ts +88 -0
  278. package/src/server/auth/providers/ServerAuthProvider.ts +197 -72
  279. package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -0
  280. package/src/server/core/__tests__/ServerRouterProvider-errorHandler.spec.ts +1 -1
  281. package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -1
  282. package/dist/billing/index.d.ts.map +0 -1
  283. package/dist/billing/index.js.map +0 -1
  284. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  285. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  286. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  287. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  288. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  289. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -7,7 +7,7 @@ import {
7
7
  CryptoProvider,
8
8
  InvalidCredentialsError,
9
9
  } from "alepha/security";
10
- import { BadRequestError } from "alepha/server";
10
+ import { BadRequestError, UnauthorizedError } from "alepha/server";
11
11
  import { describe, it } from "vitest";
12
12
  import {
13
13
  AlephaApiUsers,
@@ -320,6 +320,32 @@ describe("alepha/api/users - SessionService.login", () => {
320
320
  ).rejects.toThrowError(InvalidCredentialsError);
321
321
  });
322
322
 
323
+ it("should reject login for disabled user", async ({ expect }) => {
324
+ const { sessionService, userService, cryptoProvider, identities } =
325
+ await setup();
326
+
327
+ const password = "disabledUserPass123!";
328
+ const hashedPassword = await cryptoProvider.hashPassword(password);
329
+
330
+ const user = await userService.users().create({
331
+ username: "disableduser",
332
+ email: "disabled@example.com",
333
+ roles: ["user"],
334
+ enabled: false,
335
+ });
336
+
337
+ await identities.create({
338
+ provider: "local",
339
+ providerUserId: "disabled@example.com",
340
+ userId: user.id,
341
+ password: hashedPassword,
342
+ });
343
+
344
+ await expect(
345
+ sessionService.login("local", "disabled@example.com", password),
346
+ ).rejects.toThrowError(InvalidCredentialsError);
347
+ });
348
+
323
349
  describe("login rate limiting", () => {
324
350
  it("should block account after max failed attempts", async ({ expect }) => {
325
351
  const { sessionService, userService, cryptoProvider, identities } =
@@ -775,4 +801,119 @@ describe("alepha/api/users - SessionService.link", () => {
775
801
 
776
802
  expect(result.id).toBe(user.id);
777
803
  });
804
+
805
+ it("should refuse auto-link when OAuth provider says email_verified is false", async ({
806
+ expect,
807
+ }) => {
808
+ const { sessionService, userService } = await setup();
809
+
810
+ await userService.users().create({
811
+ username: "verifieduser",
812
+ email: "verified@example.com",
813
+ roles: ["user"],
814
+ });
815
+
816
+ await expect(
817
+ sessionService.link("google", {
818
+ sub: "google-unverified",
819
+ email: "verified@example.com",
820
+ name: "Unverified User",
821
+ email_verified: false,
822
+ }),
823
+ ).rejects.toThrowError(BadRequestError);
824
+ });
825
+
826
+ it("should allow auto-link when email_verified is true or undefined", async ({
827
+ expect,
828
+ }) => {
829
+ const { sessionService, userService } = await setup();
830
+
831
+ const user = await userService.users().create({
832
+ username: "linkableuser",
833
+ email: "linkable@example.com",
834
+ roles: ["user"],
835
+ });
836
+
837
+ const result = await sessionService.link("google", {
838
+ sub: "google-verified",
839
+ email: "linkable@example.com",
840
+ name: "Verified User",
841
+ email_verified: true,
842
+ });
843
+
844
+ expect(result.id).toBe(user.id);
845
+ });
846
+
847
+ it("should use defaultRoles from realm settings for new OAuth users", async ({
848
+ expect,
849
+ }) => {
850
+ const { sessionService, alepha } = await setup();
851
+
852
+ const realmProvider = alepha.inject(RealmProvider);
853
+ realmProvider.register("custom-roles", {
854
+ settings: {
855
+ registrationAllowed: true,
856
+ defaultRoles: ["member", "viewer"],
857
+ } as never,
858
+ });
859
+
860
+ const result = await sessionService.link(
861
+ "google",
862
+ {
863
+ sub: "google-newuser-roles",
864
+ email: "newuser-roles@example.com",
865
+ name: "New Role User",
866
+ },
867
+ "custom-roles",
868
+ );
869
+
870
+ expect("roles" in result && result.roles).toEqual(["member", "viewer"]);
871
+ });
872
+ });
873
+
874
+ describe("alepha/api/users - SessionService.refreshSession", () => {
875
+ it("should reject refresh for disabled user and delete session", async ({
876
+ expect,
877
+ }) => {
878
+ const { sessionService, userService, cryptoProvider, identities } =
879
+ await setup();
880
+
881
+ const password = "refreshDisabledPass!";
882
+ const hashedPassword = await cryptoProvider.hashPassword(password);
883
+
884
+ const user = await userService.users().create({
885
+ username: "refreshdisableduser",
886
+ email: "refresh-disabled@example.com",
887
+ roles: ["user"],
888
+ });
889
+
890
+ await identities.create({
891
+ provider: "local",
892
+ providerUserId: "refresh-disabled@example.com",
893
+ userId: user.id,
894
+ password: hashedPassword,
895
+ });
896
+
897
+ // Login to get a session with refresh token
898
+ await sessionService.login(
899
+ "local",
900
+ "refresh-disabled@example.com",
901
+ password,
902
+ );
903
+
904
+ const { refreshToken } = await sessionService.createSession(user, 3600);
905
+
906
+ // Disable the user
907
+ await userService.users().updateById(user.id, { enabled: false });
908
+
909
+ // Refresh should fail with UnauthorizedError
910
+ await expect(
911
+ sessionService.refreshSession(refreshToken),
912
+ ).rejects.toThrowError(UnauthorizedError);
913
+
914
+ // Session should be deleted — a second refresh should also fail
915
+ await expect(
916
+ sessionService.refreshSession(refreshToken),
917
+ ).rejects.toThrowError();
918
+ });
778
919
  });
@@ -70,6 +70,15 @@ export const realmAuthSettingsAtom = $atom({
70
70
  description:
71
71
  "List of usernames that are automatically promoted to admin role on login",
72
72
  }),
73
+ defaultRoles: t.array(t.string(), {
74
+ description: "Default roles assigned to newly registered users",
75
+ }),
76
+ verifyEmailUrl: t.optional(
77
+ t.string({
78
+ description:
79
+ "Base URL for email verification links (used when verification method is 'link'). Token and email are appended as query params.",
80
+ }),
81
+ ),
73
82
  passwordPolicy: t.object({
74
83
  minLength: t.integer({
75
84
  description: "Minimum password length",
@@ -122,7 +131,7 @@ export const realmAuthSettingsAtom = $atom({
122
131
  firstNameLastName: "none" as FieldRequirement,
123
132
  adminEmails: [],
124
133
  adminUsernames: [],
125
- // TODO: not implemented yet
134
+ defaultRoles: ["user"],
126
135
  passwordPolicy: {
127
136
  minLength: 8,
128
137
  requireUppercase: true,
@@ -1,4 +1,5 @@
1
1
  import { $inject, t } from "alepha";
2
+ import { $secure } from "alepha/security";
2
3
  import { $action, okSchema } from "alepha/server";
3
4
  import { completePasswordResetRequestSchema } from "../schemas/completePasswordResetRequestSchema.ts";
4
5
  import { completeRegistrationRequestSchema } from "../schemas/completeRegistrationRequestSchema.ts";
@@ -216,13 +217,7 @@ export class UserController {
216
217
  t.enum(["code", "link"], {
217
218
  default: "code",
218
219
  description:
219
- 'Verification method: "code" sends a 6-digit code, "link" sends a clickable verification link.',
220
- }),
221
- ),
222
- verifyUrl: t.optional(
223
- t.string({
224
- description:
225
- 'Base URL for verification link. Required when method is "link". Token and email will be appended as query params.',
220
+ 'Verification method: "code" sends a 6-digit code, "link" sends a clickable verification link. When using "link", configure verifyEmailUrl in realm settings.',
226
221
  }),
227
222
  ),
228
223
  }),
@@ -240,7 +235,6 @@ export class UserController {
240
235
  body.email,
241
236
  query.userRealmName,
242
237
  method,
243
- query.verifyUrl,
244
238
  );
245
239
 
246
240
  return {
@@ -293,6 +287,7 @@ export class UserController {
293
287
  public checkEmailVerification = $action({
294
288
  path: "/users/email-verification/check",
295
289
  group: this.group,
290
+ use: [$secure()],
296
291
  schema: {
297
292
  query: t.object({
298
293
  email: t.email(),
@@ -109,6 +109,29 @@ export class UserNotifications {
109
109
  }),
110
110
  });
111
111
 
112
+ public readonly accountLockout = $notification({
113
+ category: "security",
114
+ description:
115
+ "Email sent to users when their account is temporarily locked due to too many failed login attempts.",
116
+ critical: true,
117
+ sensitive: true,
118
+ email: {
119
+ subject: "Account temporarily locked",
120
+ body: (it) => `
121
+ <h1>Account Temporarily Locked</h1>
122
+ <p>Hi ${it.email},</p>
123
+ <p>Your account has been temporarily locked due to too many failed login attempts.</p>
124
+ <p>If this was you, please wait ${it.lockoutMinutes} minutes before trying again. If you've forgotten your password, you can reset it using the password reset feature.</p>
125
+ <p>If this wasn't you, someone may be trying to access your account. We recommend changing your password as soon as possible.</p>
126
+ <p>Best regards,<br>The Team</p>
127
+ `,
128
+ },
129
+ schema: t.object({
130
+ email: t.string({ format: "email" }),
131
+ lockoutMinutes: t.number(),
132
+ }),
133
+ });
134
+
112
135
  public readonly emailVerificationLink = $notification({
113
136
  category: "security",
114
137
  description:
@@ -12,9 +12,13 @@ import {
12
12
  SecurityProvider,
13
13
  } from "alepha/security";
14
14
  import {
15
+ $authApple,
15
16
  $authCredentials,
17
+ $authFacebook,
18
+ $authFranceConnect,
16
19
  $authGithub,
17
20
  $authGoogle,
21
+ $authMicrosoft,
18
22
  type AuthPrimitive,
19
23
  type Credentials,
20
24
  type LinkAccountOptions,
@@ -205,6 +209,22 @@ export const $realm = (options: RealmOptions = {}): RealmPrimitive => {
205
209
  auth.github = $authGithub(realm);
206
210
  }
207
211
 
212
+ if (identities.apple) {
213
+ auth.apple = $authApple(realm);
214
+ }
215
+
216
+ if (identities.facebook) {
217
+ auth.facebook = $authFacebook(realm);
218
+ }
219
+
220
+ if (identities.microsoft) {
221
+ auth.microsoft = $authMicrosoft(realm);
222
+ }
223
+
224
+ if (identities.franceconnect) {
225
+ auth.franceconnect = $authFranceConnect(realm);
226
+ }
227
+
208
228
  alepha.with(() => auth);
209
229
  }
210
230
 
@@ -315,6 +335,10 @@ export interface RealmOptions {
315
335
  credentials?: true;
316
336
  google?: true;
317
337
  github?: true;
338
+ apple?: true;
339
+ facebook?: true;
340
+ microsoft?: true;
341
+ franceconnect?: true;
318
342
  };
319
343
 
320
344
  /**
@@ -8,7 +8,7 @@ export const loginSchema = t.object({
8
8
  description: "Username or email address for login",
9
9
  }),
10
10
  password: t.text({
11
- minLength: 6,
11
+ minLength: 8,
12
12
  description: "User password",
13
13
  }),
14
14
  });
@@ -7,6 +7,7 @@ import { $logger } from "alepha/logger";
7
7
  import { CryptoProvider } from "alepha/security";
8
8
  import { BadRequestError, HttpError } from "alepha/server";
9
9
  import { $client } from "alepha/server/links";
10
+ import type { RealmAuthSettings } from "../atoms/realmAuthSettingsAtom.ts";
10
11
  import { UserAudits } from "../audits/UserAudits.ts";
11
12
  import { UserNotifications } from "../notifications/UserNotifications.ts";
12
13
  import { RealmProvider } from "../providers/RealmProvider.ts";
@@ -67,6 +68,38 @@ export class CredentialService {
67
68
  return this.realmProvider.identityRepository(userRealmName);
68
69
  }
69
70
 
71
+ /**
72
+ * Validate a password against the realm's password policy.
73
+ */
74
+ public validatePasswordPolicy(
75
+ password: string,
76
+ policy: RealmAuthSettings["passwordPolicy"],
77
+ ): void {
78
+ if (password.length < policy.minLength) {
79
+ throw new BadRequestError(
80
+ `Password must be at least ${policy.minLength} characters`,
81
+ );
82
+ }
83
+ if (policy.requireUppercase && !/[A-Z]/.test(password)) {
84
+ throw new BadRequestError(
85
+ "Password must contain at least one uppercase letter",
86
+ );
87
+ }
88
+ if (policy.requireLowercase && !/[a-z]/.test(password)) {
89
+ throw new BadRequestError(
90
+ "Password must contain at least one lowercase letter",
91
+ );
92
+ }
93
+ if (policy.requireNumbers && !/\d/.test(password)) {
94
+ throw new BadRequestError("Password must contain at least one number");
95
+ }
96
+ if (policy.requireSpecialCharacters && !/[^a-zA-Z0-9]/.test(password)) {
97
+ throw new BadRequestError(
98
+ "Password must contain at least one special character",
99
+ );
100
+ }
101
+ }
102
+
70
103
  /**
71
104
  * Phase 1: Create a password reset intent.
72
105
  *
@@ -90,6 +123,14 @@ export class CredentialService {
90
123
  .add(INTENT_TTL_MINUTES, "minutes")
91
124
  .toISOString();
92
125
 
126
+ // Check if password reset is allowed for this realm
127
+ const realm = this.realmProvider.getRealm(userRealmName);
128
+ const realmSettings = await realm.getSettings();
129
+ if (realmSettings.resetPasswordAllowed === false) {
130
+ this.log.debug("Password reset not allowed for realm", { userRealmName });
131
+ return { intentId, expiresAt };
132
+ }
133
+
93
134
  // Find user by email (silent fail for security)
94
135
  const user = await this.users(userRealmName).findOne({
95
136
  where: { email: { eq: email } },
@@ -187,6 +228,11 @@ export class CredentialService {
187
228
  });
188
229
  }
189
230
 
231
+ // Validate password against realm policy before consuming the verification code
232
+ const realm = this.realmProvider.getRealm(intent.realmName);
233
+ const realmSettings = await realm.getSettings();
234
+ this.validatePasswordPolicy(body.newPassword, realmSettings.passwordPolicy);
235
+
190
236
  // Verify code using verification controller
191
237
  const result = await this.verificationController
192
238
  .validateVerificationCode({
@@ -210,9 +256,6 @@ export class CredentialService {
210
256
  throw new BadRequestError("Verification code has already been used");
211
257
  }
212
258
 
213
- // Atomically delete cache key to prevent replay
214
- await this.intentCache.invalidate(body.intentId);
215
-
216
259
  // Hash the new password
217
260
  const hashedPassword = await this.cryptoProvider.hashPassword(
218
261
  body.newPassword,
@@ -228,13 +271,17 @@ export class CredentialService {
228
271
  userId: { eq: intent.userId },
229
272
  });
230
273
 
274
+ // Invalidate intent after all operations succeed,
275
+ // so the user can retry if password update or session cleanup fails.
276
+ // The verification code was already consumed (verifiedAt set), so replay is
277
+ // prevented even if the intent is still in cache.
278
+ await this.intentCache.invalidate(body.intentId);
279
+
231
280
  this.log.info("Password reset completed", {
232
281
  userId: intent.userId,
233
282
  email: intent.email,
234
283
  });
235
284
 
236
- const realm = this.realmProvider.getRealm(intent.realmName);
237
-
238
285
  // Audit: password reset
239
286
  await this.userAudits(intent.realmName)?.recordUser("update", {
240
287
  userId: intent.userId,
@@ -320,6 +367,11 @@ export class CredentialService {
320
367
  throw new BadRequestError("Invalid or expired reset token");
321
368
  }
322
369
 
370
+ // Validate password against realm policy
371
+ const realm = this.realmProvider.getRealm(userRealmName);
372
+ const realmSettings = await realm.getSettings();
373
+ this.validatePasswordPolicy(newPassword, realmSettings.passwordPolicy);
374
+
323
375
  // Find user and identity
324
376
  const user = await this.users(userRealmName).getOne({
325
377
  where: { email: { eq: email } },
@@ -345,8 +397,6 @@ export class CredentialService {
345
397
  userId: { eq: user.id },
346
398
  });
347
399
 
348
- const realm = this.realmProvider.getRealm(userRealmName);
349
-
350
400
  // Audit: password reset (legacy method)
351
401
  await this.userAudits(userRealmName)?.recordUser("update", {
352
402
  userId: user.id,
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { $inject, Alepha, AlephaError } from "alepha";
3
3
  import type { VerificationController } from "alepha/api/verifications";
4
4
  import { $cache } from "alepha/cache";
5
+ import { CaptchaProvider } from "alepha/captcha";
5
6
  import { DateTimeProvider } from "alepha/datetime";
6
7
  import { $logger } from "alepha/logger";
7
8
  import { CryptoProvider } from "alepha/security";
@@ -14,6 +15,7 @@ import { RealmProvider } from "../providers/RealmProvider.ts";
14
15
  import type { CompleteRegistrationRequest } from "../schemas/completeRegistrationRequestSchema.ts";
15
16
  import type { RegisterRequest } from "../schemas/registerRequestSchema.ts";
16
17
  import type { RegistrationIntentResponse } from "../schemas/registrationIntentResponseSchema.ts";
18
+ import { CredentialService } from "./CredentialService.ts";
17
19
 
18
20
  /**
19
21
  * Intent stored in cache during the registration flow.
@@ -46,12 +48,19 @@ export class RegistrationService {
46
48
  protected readonly cryptoProvider = $inject(CryptoProvider);
47
49
  protected readonly verificationController = $client<VerificationController>();
48
50
  protected readonly realmProvider = $inject(RealmProvider);
51
+ protected readonly credentialService = $inject(CredentialService);
52
+ protected readonly captchaProvider = $inject(CaptchaProvider);
49
53
 
50
54
  protected readonly intentCache = $cache<RegistrationIntent>({
51
55
  name: "api:users:registrations",
52
56
  ttl: [INTENT_TTL_MINUTES, "minutes"],
53
57
  });
54
58
 
59
+ protected readonly rateLimitCache = $cache<number>({
60
+ name: "api:users:registration-rate-limit",
61
+ ttl: [15, "minutes"],
62
+ });
63
+
55
64
  protected userAudits(realmName?: string) {
56
65
  const realm = this.realmProvider.getRealm(realmName);
57
66
  if (realm.features.audits) {
@@ -88,6 +97,20 @@ export class RegistrationService {
88
97
  userRealmName,
89
98
  });
90
99
 
100
+ // IP rate limiting
101
+ const request = this.alepha.store.get("alepha.http.request");
102
+ const ipKey = request?.ip ? `register:ip:${request.ip}` : undefined;
103
+ if (ipKey) {
104
+ const count = (await this.rateLimitCache.get(ipKey)) ?? 0;
105
+ if (count >= 10) {
106
+ this.log.warn("Registration rate limit exceeded", { ip: request?.ip });
107
+ throw new BadRequestError(
108
+ "Too many registration attempts, please try again later",
109
+ );
110
+ }
111
+ await this.rateLimitCache.set(ipKey, count + 1);
112
+ }
113
+
91
114
  const realm = this.realmProvider.getRealm(userRealmName);
92
115
  const realmSettings = await realm.getSettings();
93
116
 
@@ -106,6 +129,9 @@ export class RegistrationService {
106
129
  }
107
130
 
108
131
  if (body.username) {
132
+ // Security note: usernameRegExp is admin-controlled (from realmAuthSettingsAtom),
133
+ // not user input. Default is ^[a-zA-Z0-9_]{3,30}$ which is ReDoS-safe.
134
+ // No need for regex timeout or safe-regex validation here.
109
135
  const usernameRegExp = realmSettings?.usernameRegExp;
110
136
  if (usernameRegExp) {
111
137
  const regex = new RegExp(usernameRegExp);
@@ -138,6 +164,12 @@ export class RegistrationService {
138
164
  // Check for existing users (username, email, phone)
139
165
  await this.checkUserAvailability(body, userRealmName);
140
166
 
167
+ // Validate password against realm policy
168
+ this.credentialService.validatePasswordPolicy(
169
+ body.password,
170
+ realmSettings.passwordPolicy,
171
+ );
172
+
141
173
  // Hash the password
142
174
  const passwordHash = await this.cryptoProvider.hashPassword(body.password);
143
175
 
@@ -261,12 +293,16 @@ export class RegistrationService {
261
293
  await this.verifyPhoneCode(intent.data.phoneNumber, body.phoneCode);
262
294
  }
263
295
 
264
- // Validate captcha if required (placeholder for future implementation)
296
+ // Validate captcha if required
265
297
  if (intent.requirements.captcha) {
266
298
  if (!body.captchaToken) {
267
299
  throw new BadRequestError("Captcha verification is required");
268
300
  }
269
- // TODO: Validate captcha token
301
+
302
+ const valid = await this.captchaProvider.verify(body.captchaToken);
303
+ if (!valid) {
304
+ throw new BadRequestError("Captcha verification failed");
305
+ }
270
306
  }
271
307
 
272
308
  // Final availability check (race condition guard)
@@ -279,19 +315,19 @@ export class RegistrationService {
279
315
  userRealmName,
280
316
  );
281
317
 
282
- // Atomically delete cache key to prevent replay
283
- await this.intentCache.invalidate(body.intentId);
318
+ const realm = this.realmProvider.getRealm(userRealmName);
319
+ const realmSettings = await realm.getSettings();
284
320
 
285
321
  // Create the user
286
322
  const user = await userRepository.create({
287
- realm: userRealmName,
323
+ realm: realm.name,
288
324
  username: intent.data.username,
289
325
  email: intent.data.email,
290
326
  phoneNumber: intent.data.phoneNumber,
291
327
  firstName: intent.data.firstName,
292
328
  lastName: intent.data.lastName,
293
329
  picture: intent.data.picture,
294
- roles: ["user"],
330
+ roles: realmSettings.defaultRoles,
295
331
  enabled: true,
296
332
  emailVerified: intent.requirements.email, // Marked as verified if we verified during registration
297
333
  });
@@ -303,14 +339,16 @@ export class RegistrationService {
303
339
  password: intent.data.passwordHash,
304
340
  });
305
341
 
342
+ // Invalidate intent after both user and identity creation succeed,
343
+ // so the user can retry if either operation fails.
344
+ await this.intentCache.invalidate(body.intentId);
345
+
306
346
  this.log.info("User registered successfully", {
307
347
  userId: user.id,
308
348
  email: user.email,
309
349
  username: user.username,
310
350
  });
311
351
 
312
- const realm = this.realmProvider.getRealm(userRealmName);
313
-
314
352
  await this.userAudits(userRealmName)?.recordUser("create", {
315
353
  userId: user.id,
316
354
  userEmail: user.email ?? undefined,
@@ -335,11 +373,12 @@ export class RegistrationService {
335
373
  body: Pick<RegisterRequest, "username" | "email" | "phoneNumber">,
336
374
  userRealmName?: string,
337
375
  ): Promise<void> {
376
+ const realm = this.realmProvider.getRealm(userRealmName);
338
377
  const userRepository = this.realmProvider.userRepository(userRealmName);
339
378
 
340
379
  if (body.username) {
341
380
  const existingUser = await userRepository.findOne({
342
- where: { username: { ilike: body.username } },
381
+ where: { realm: realm.name, username: { ilike: body.username } },
343
382
  });
344
383
  if (existingUser) {
345
384
  this.log.debug("Username already taken", { username: body.username });
@@ -349,7 +388,7 @@ export class RegistrationService {
349
388
 
350
389
  if (body.email) {
351
390
  const existingUser = await userRepository.findOne({
352
- where: { email: { eq: body.email } },
391
+ where: { realm: realm.name, email: { eq: body.email } },
353
392
  });
354
393
  if (existingUser) {
355
394
  this.log.debug("Email already taken", { email: body.email });
@@ -359,7 +398,7 @@ export class RegistrationService {
359
398
 
360
399
  if (body.phoneNumber) {
361
400
  const existingUser = await userRepository.findOne({
362
- where: { phoneNumber: { eq: body.phoneNumber } },
401
+ where: { realm: realm.name, phoneNumber: { eq: body.phoneNumber } },
363
402
  });
364
403
  if (existingUser) {
365
404
  this.log.debug("Phone number already taken", {