alepha 0.15.3 → 0.15.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 (318) hide show
  1. package/README.md +26 -11
  2. package/dist/api/audits/index.d.ts +335 -335
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +11 -3
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +3 -3
  7. package/dist/api/files/index.js +4 -3
  8. package/dist/api/files/index.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +198 -155
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +103 -5
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/keys/index.d.ts +198 -198
  14. package/dist/api/keys/index.d.ts.map +1 -1
  15. package/dist/api/keys/index.js +3 -3
  16. package/dist/api/keys/index.js.map +1 -1
  17. package/dist/api/notifications/index.browser.js +1 -0
  18. package/dist/api/notifications/index.browser.js.map +1 -1
  19. package/dist/api/notifications/index.d.ts +3 -3
  20. package/dist/api/notifications/index.js +4 -3
  21. package/dist/api/notifications/index.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +263 -263
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +41 -30
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/api/users/index.d.ts +383 -77
  27. package/dist/api/users/index.d.ts.map +1 -1
  28. package/dist/api/users/index.js +284 -72
  29. package/dist/api/users/index.js.map +1 -1
  30. package/dist/api/verifications/index.d.ts +131 -131
  31. package/dist/api/verifications/index.d.ts.map +1 -1
  32. package/dist/api/verifications/index.js +3 -3
  33. package/dist/api/verifications/index.js.map +1 -1
  34. package/dist/batch/index.d.ts +3 -3
  35. package/dist/batch/index.js +3 -3
  36. package/dist/batch/index.js.map +1 -1
  37. package/dist/bucket/index.d.ts +3 -3
  38. package/dist/bucket/index.js +6 -6
  39. package/dist/bucket/index.js.map +1 -1
  40. package/dist/cache/core/index.d.ts +3 -3
  41. package/dist/cache/core/index.js +3 -3
  42. package/dist/cache/core/index.js.map +1 -1
  43. package/dist/cli/index.d.ts +5612 -20
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +122 -91
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/command/index.d.ts +11 -4
  48. package/dist/command/index.d.ts.map +1 -1
  49. package/dist/command/index.js +8 -6
  50. package/dist/command/index.js.map +1 -1
  51. package/dist/core/index.browser.js.map +1 -1
  52. package/dist/core/index.d.ts +4 -8
  53. package/dist/core/index.d.ts.map +1 -1
  54. package/dist/core/index.js +3 -3
  55. package/dist/core/index.js.map +1 -1
  56. package/dist/core/index.native.js.map +1 -1
  57. package/dist/datetime/index.d.ts +3 -3
  58. package/dist/datetime/index.js +3 -3
  59. package/dist/datetime/index.js.map +1 -1
  60. package/dist/email/index.d.ts +16 -16
  61. package/dist/email/index.d.ts.map +1 -1
  62. package/dist/email/index.js +10562 -10
  63. package/dist/email/index.js.map +1 -1
  64. package/dist/fake/index.d.ts +3 -3
  65. package/dist/fake/index.js +3 -3
  66. package/dist/fake/index.js.map +1 -1
  67. package/dist/lock/core/index.d.ts +9 -4
  68. package/dist/lock/core/index.d.ts.map +1 -1
  69. package/dist/lock/core/index.js +12 -4
  70. package/dist/lock/core/index.js.map +1 -1
  71. package/dist/logger/index.d.ts +3 -3
  72. package/dist/logger/index.js +6 -3
  73. package/dist/logger/index.js.map +1 -1
  74. package/dist/mcp/index.d.ts +3 -3
  75. package/dist/mcp/index.js +3 -3
  76. package/dist/mcp/index.js.map +1 -1
  77. package/dist/orm/index.d.ts +12 -12
  78. package/dist/orm/index.js +4 -4
  79. package/dist/orm/index.js.map +1 -1
  80. package/dist/queue/core/index.d.ts +3 -3
  81. package/dist/queue/core/index.js +3 -3
  82. package/dist/queue/core/index.js.map +1 -1
  83. package/dist/react/auth/index.browser.js +2 -1
  84. package/dist/react/auth/index.browser.js.map +1 -1
  85. package/dist/react/auth/index.d.ts +3 -3
  86. package/dist/react/auth/index.js +5 -4
  87. package/dist/react/auth/index.js.map +1 -1
  88. package/dist/react/core/index.d.ts +6 -6
  89. package/dist/react/core/index.js +3 -3
  90. package/dist/react/core/index.js.map +1 -1
  91. package/dist/react/form/index.d.ts +3 -3
  92. package/dist/react/form/index.js +3 -3
  93. package/dist/react/form/index.js.map +1 -1
  94. package/dist/react/head/index.d.ts +3 -3
  95. package/dist/react/head/index.js +3 -3
  96. package/dist/react/head/index.js.map +1 -1
  97. package/dist/react/i18n/index.d.ts +3 -3
  98. package/dist/react/i18n/index.js +3 -3
  99. package/dist/react/i18n/index.js.map +1 -1
  100. package/dist/react/intro/index.css +337 -0
  101. package/dist/react/intro/index.css.map +1 -0
  102. package/dist/react/intro/index.d.ts +10 -0
  103. package/dist/react/intro/index.d.ts.map +1 -0
  104. package/dist/react/intro/index.js +222 -0
  105. package/dist/react/intro/index.js.map +1 -0
  106. package/dist/react/router/index.browser.js +2 -2
  107. package/dist/react/router/index.browser.js.map +1 -1
  108. package/dist/react/router/index.d.ts +11 -1
  109. package/dist/react/router/index.d.ts.map +1 -1
  110. package/dist/react/router/index.js +21 -11
  111. package/dist/react/router/index.js.map +1 -1
  112. package/dist/redis/index.d.ts +22 -22
  113. package/dist/redis/index.js +3 -3
  114. package/dist/redis/index.js.map +1 -1
  115. package/dist/retry/index.d.ts +3 -3
  116. package/dist/retry/index.js +3 -3
  117. package/dist/retry/index.js.map +1 -1
  118. package/dist/scheduler/index.d.ts +16 -4
  119. package/dist/scheduler/index.d.ts.map +1 -1
  120. package/dist/scheduler/index.js +45 -7
  121. package/dist/scheduler/index.js.map +1 -1
  122. package/dist/security/index.d.ts +3 -3
  123. package/dist/security/index.js +5 -5
  124. package/dist/security/index.js.map +1 -1
  125. package/dist/server/auth/index.d.ts +3 -3
  126. package/dist/server/auth/index.js +3 -3
  127. package/dist/server/auth/index.js.map +1 -1
  128. package/dist/server/cache/index.d.ts +3 -3
  129. package/dist/server/cache/index.js +3 -3
  130. package/dist/server/cache/index.js.map +1 -1
  131. package/dist/server/compress/index.d.ts +3 -3
  132. package/dist/server/compress/index.d.ts.map +1 -1
  133. package/dist/server/compress/index.js +4 -3
  134. package/dist/server/compress/index.js.map +1 -1
  135. package/dist/server/cookies/index.d.ts +3 -3
  136. package/dist/server/cookies/index.js +3 -3
  137. package/dist/server/cookies/index.js.map +1 -1
  138. package/dist/server/core/index.d.ts +14 -25
  139. package/dist/server/core/index.d.ts.map +1 -1
  140. package/dist/server/core/index.js +13 -29
  141. package/dist/server/core/index.js.map +1 -1
  142. package/dist/server/cors/index.d.ts +3 -3
  143. package/dist/server/cors/index.js +3 -3
  144. package/dist/server/cors/index.js.map +1 -1
  145. package/dist/server/health/index.d.ts +20 -20
  146. package/dist/server/health/index.js +3 -3
  147. package/dist/server/health/index.js.map +1 -1
  148. package/dist/server/helmet/index.d.ts +3 -3
  149. package/dist/server/helmet/index.js +3 -3
  150. package/dist/server/helmet/index.js.map +1 -1
  151. package/dist/server/links/index.d.ts +42 -42
  152. package/dist/server/links/index.d.ts.map +1 -1
  153. package/dist/server/links/index.js +4 -4
  154. package/dist/server/links/index.js.map +1 -1
  155. package/dist/server/metrics/index.d.ts +3 -3
  156. package/dist/server/metrics/index.js +3 -3
  157. package/dist/server/metrics/index.js.map +1 -1
  158. package/dist/server/multipart/index.d.ts +3 -3
  159. package/dist/server/multipart/index.js +3 -3
  160. package/dist/server/multipart/index.js.map +1 -1
  161. package/dist/server/proxy/index.d.ts +3 -3
  162. package/dist/server/proxy/index.js +3 -3
  163. package/dist/server/proxy/index.js.map +1 -1
  164. package/dist/server/rate-limit/index.d.ts +3 -3
  165. package/dist/server/rate-limit/index.js +3 -3
  166. package/dist/server/rate-limit/index.js.map +1 -1
  167. package/dist/server/static/index.d.ts +3 -3
  168. package/dist/server/static/index.js +6 -6
  169. package/dist/server/static/index.js.map +1 -1
  170. package/dist/server/swagger/index.d.ts +3 -3
  171. package/dist/server/swagger/index.js +6 -6
  172. package/dist/server/swagger/index.js.map +1 -1
  173. package/dist/sms/index.d.ts +3 -3
  174. package/dist/sms/index.js +6 -6
  175. package/dist/sms/index.js.map +1 -1
  176. package/dist/system/index.d.ts +3 -3
  177. package/dist/system/index.js +3 -3
  178. package/dist/system/index.js.map +1 -1
  179. package/dist/thread/index.d.ts +3 -3
  180. package/dist/thread/index.js +3 -3
  181. package/dist/thread/index.js.map +1 -1
  182. package/dist/topic/core/index.d.ts +3 -3
  183. package/dist/topic/core/index.js +3 -3
  184. package/dist/topic/core/index.js.map +1 -1
  185. package/dist/vite/index.d.ts +6286 -4
  186. package/dist/vite/index.d.ts.map +1 -1
  187. package/dist/vite/index.js +28 -2
  188. package/dist/vite/index.js.map +1 -1
  189. package/dist/websocket/index.d.ts +37 -37
  190. package/dist/websocket/index.d.ts.map +1 -1
  191. package/dist/websocket/index.js +3 -3
  192. package/dist/websocket/index.js.map +1 -1
  193. package/package.json +12 -4
  194. package/src/api/audits/controllers/AdminAuditController.ts +8 -0
  195. package/src/api/audits/index.ts +3 -3
  196. package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
  197. package/src/api/files/index.ts +3 -3
  198. package/src/api/jobs/controllers/AdminJobController.ts +18 -2
  199. package/src/api/jobs/index.ts +4 -3
  200. package/src/api/jobs/services/JobAudits.spec.ts +89 -0
  201. package/src/api/jobs/services/JobAudits.ts +101 -0
  202. package/src/api/keys/index.ts +3 -3
  203. package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
  204. package/src/api/notifications/index.ts +3 -3
  205. package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
  206. package/src/api/parameters/index.ts +5 -3
  207. package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1 -1
  208. package/src/api/users/__tests__/ApiKeys.spec.ts +1 -1
  209. package/src/api/users/__tests__/EmailVerification.spec.ts +16 -1
  210. package/src/api/users/__tests__/PasswordReset.spec.ts +11 -0
  211. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -0
  212. package/src/api/users/controllers/AdminIdentityController.ts +3 -0
  213. package/src/api/users/controllers/AdminSessionController.ts +3 -0
  214. package/src/api/users/controllers/AdminUserController.ts +5 -0
  215. package/src/api/users/index.ts +8 -9
  216. package/src/api/users/primitives/$realm.ts +117 -19
  217. package/src/api/users/providers/RealmProvider.ts +15 -7
  218. package/src/api/users/services/CredentialService.spec.ts +11 -0
  219. package/src/api/users/services/CredentialService.ts +47 -24
  220. package/src/api/users/services/IdentityService.ts +12 -4
  221. package/src/api/users/services/RegistrationService.spec.ts +11 -0
  222. package/src/api/users/services/RegistrationService.ts +33 -12
  223. package/src/api/users/services/SessionService.ts +83 -12
  224. package/src/api/users/services/UserAudits.ts +47 -0
  225. package/src/api/users/services/UserFiles.ts +19 -0
  226. package/src/api/users/services/UserJobs.spec.ts +107 -0
  227. package/src/api/users/services/UserJobs.ts +62 -0
  228. package/src/api/users/services/UserParameters.ts +23 -0
  229. package/src/api/users/services/UserService.ts +34 -17
  230. package/src/api/verifications/index.ts +3 -3
  231. package/src/batch/index.ts +3 -3
  232. package/src/bucket/index.ts +3 -3
  233. package/src/cache/core/index.ts +3 -3
  234. package/src/cli/commands/build.ts +1 -0
  235. package/src/cli/commands/db.ts +9 -0
  236. package/src/cli/commands/init.spec.ts +2 -17
  237. package/src/cli/commands/init.ts +37 -1
  238. package/src/cli/providers/ViteDevServerProvider.ts +36 -2
  239. package/src/cli/services/AlephaCliUtils.ts +17 -0
  240. package/src/cli/services/PackageManagerUtils.ts +15 -1
  241. package/src/cli/services/ProjectScaffolder.ts +8 -13
  242. package/src/cli/templates/agentMd.ts +2 -25
  243. package/src/cli/templates/apiAppSecurityTs.ts +37 -2
  244. package/src/cli/templates/mainCss.ts +2 -32
  245. package/src/cli/templates/webAppRouterTs.ts +5 -5
  246. package/src/cli/templates/webHomeComponentTsx.ts +10 -0
  247. package/src/command/helpers/Runner.ts +14 -1
  248. package/src/command/index.ts +3 -3
  249. package/src/core/helpers/primitive.ts +0 -5
  250. package/src/core/index.ts +3 -3
  251. package/src/datetime/index.ts +3 -3
  252. package/src/email/index.ts +3 -3
  253. package/src/email/index.workerd.ts +36 -0
  254. package/src/email/providers/LocalEmailProvider.ts +2 -2
  255. package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
  256. package/src/fake/index.ts +3 -3
  257. package/src/lock/core/index.ts +3 -3
  258. package/src/lock/core/primitives/$lock.ts +13 -1
  259. package/src/logger/index.ts +3 -3
  260. package/src/logger/providers/PrettyFormatterProvider.ts +7 -0
  261. package/src/mcp/index.ts +3 -3
  262. package/src/orm/index.ts +3 -3
  263. package/src/orm/providers/drivers/NodeSqliteProvider.ts +1 -1
  264. package/src/queue/core/index.ts +3 -3
  265. package/src/react/auth/index.ts +3 -3
  266. package/src/react/auth/services/ReactAuth.ts +3 -1
  267. package/src/react/core/index.ts +3 -3
  268. package/src/react/form/index.ts +3 -3
  269. package/src/react/head/index.ts +3 -3
  270. package/src/react/i18n/index.ts +3 -3
  271. package/src/react/intro/components/GettingStarted.css +334 -0
  272. package/src/react/intro/components/GettingStarted.tsx +276 -0
  273. package/src/react/intro/index.ts +1 -0
  274. package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
  275. package/src/react/router/index.browser.ts +2 -0
  276. package/src/react/router/index.ts +2 -0
  277. package/src/react/router/providers/ReactServerProvider.ts +14 -4
  278. package/src/react/router/providers/SSRManifestProvider.ts +7 -0
  279. package/src/redis/index.ts +3 -3
  280. package/src/retry/index.ts +3 -3
  281. package/src/router/index.ts +3 -3
  282. package/src/scheduler/index.ts +3 -3
  283. package/src/scheduler/index.workerd.ts +43 -0
  284. package/src/scheduler/providers/CronProvider.ts +53 -6
  285. package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
  286. package/src/security/index.ts +3 -3
  287. package/src/security/providers/JwtProvider.ts +2 -2
  288. package/src/server/auth/index.ts +3 -3
  289. package/src/server/cache/index.ts +3 -3
  290. package/src/server/compress/index.ts +3 -3
  291. package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
  292. package/src/server/cookies/index.ts +3 -3
  293. package/src/server/core/index.ts +3 -3
  294. package/src/server/core/primitives/$action.spec.ts +3 -2
  295. package/src/server/core/primitives/$action.ts +6 -2
  296. package/src/server/core/providers/NodeHttpServerProvider.ts +2 -15
  297. package/src/server/core/providers/ServerProvider.ts +4 -2
  298. package/src/server/core/providers/ServerRouterProvider.ts +5 -27
  299. package/src/server/cors/index.ts +3 -3
  300. package/src/server/health/index.ts +3 -3
  301. package/src/server/helmet/index.ts +3 -3
  302. package/src/server/links/index.ts +3 -3
  303. package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
  304. package/src/server/links/providers/ServerLinksProvider.ts +1 -1
  305. package/src/server/metrics/index.ts +3 -3
  306. package/src/server/multipart/index.ts +3 -3
  307. package/src/server/proxy/index.ts +3 -3
  308. package/src/server/rate-limit/index.ts +3 -3
  309. package/src/server/static/index.ts +3 -3
  310. package/src/server/swagger/index.ts +3 -3
  311. package/src/sms/index.ts +3 -3
  312. package/src/system/index.ts +3 -3
  313. package/src/thread/index.ts +3 -3
  314. package/src/topic/core/index.ts +3 -3
  315. package/src/vite/tasks/generateCloudflare.ts +38 -2
  316. package/src/websocket/index.ts +3 -3
  317. package/src/cli/templates/webHelloComponentTsx.ts +0 -30
  318. /package/src/api/users/{notifications → services}/UserNotifications.ts +0 -0
@@ -1,6 +1,5 @@
1
1
  import { randomInt } from "node:crypto";
2
2
  import { $inject, Alepha } from "alepha";
3
- import { AuditService } from "alepha/api/audits";
4
3
  import type { FileController } from "alepha/api/files";
5
4
  import { DateTimeProvider } from "alepha/datetime";
6
5
  import { $logger } from "alepha/logger";
@@ -15,6 +14,7 @@ import { $client } from "alepha/server/links";
15
14
  import { FileSystemProvider } from "alepha/system";
16
15
  import type { UserEntity } from "../entities/users.ts";
17
16
  import { RealmProvider } from "../providers/RealmProvider.ts";
17
+ import { UserAudits } from "./UserAudits.ts";
18
18
 
19
19
  export class SessionService {
20
20
  protected readonly alepha = $inject(Alepha);
@@ -24,7 +24,14 @@ export class SessionService {
24
24
  protected readonly log = $logger();
25
25
  protected readonly realmProvider = $inject(RealmProvider);
26
26
  protected readonly fileController = $client<FileController>();
27
- protected readonly auditService = $inject(AuditService);
27
+
28
+ protected userAudits(realmName?: string) {
29
+ const realm = this.realmProvider.getRealm(realmName);
30
+ if (realm.features.audits) {
31
+ return this.alepha.inject(UserAudits);
32
+ }
33
+ return undefined;
34
+ }
28
35
 
29
36
  public users(userRealmName?: string) {
30
37
  return this.realmProvider.userRepository(userRealmName);
@@ -38,6 +45,55 @@ export class SessionService {
38
45
  return this.realmProvider.identityRepository(userRealmName);
39
46
  }
40
47
 
48
+ /**
49
+ * Check if user should be auto-promoted to admin based on adminEmails/adminUsernames settings.
50
+ * If user matches and doesn't have admin role, promote them.
51
+ */
52
+ protected async ensureAdminRole(
53
+ user: {
54
+ id: string;
55
+ email?: string | null;
56
+ username?: string | null;
57
+ roles: string[];
58
+ },
59
+ userRealmName?: string,
60
+ ): Promise<boolean> {
61
+ if (user.roles.includes("admin")) return false;
62
+
63
+ const { settings, name } = this.realmProvider.getRealm(userRealmName);
64
+ const adminEmails = settings.adminEmails ?? [];
65
+ const adminUsernames = settings.adminUsernames ?? [];
66
+
67
+ const isAdminByEmail = user.email && adminEmails.includes(user.email);
68
+ const isAdminByUsername =
69
+ user.username && adminUsernames.includes(user.username);
70
+
71
+ if (!isAdminByEmail && !isAdminByUsername) return false;
72
+
73
+ // Promote to admin
74
+ user.roles = [...user.roles.filter((r) => r !== "admin"), "admin"];
75
+ await this.users(userRealmName).updateById(user.id, { roles: user.roles });
76
+
77
+ const reason = isAdminByEmail ? "adminEmails" : "adminUsernames";
78
+ this.log.info(`User auto-promoted to admin via ${reason} setting`, {
79
+ userId: user.id,
80
+ email: user.email,
81
+ username: user.username,
82
+ realm: name,
83
+ });
84
+
85
+ await this.userAudits(userRealmName)?.recordUser("role_change", {
86
+ userId: user.id,
87
+ userEmail: user.email ?? undefined,
88
+ userRealm: name,
89
+ resourceId: user.id,
90
+ description: `User auto-promoted to admin via ${reason} setting`,
91
+ metadata: { addedRole: "admin", reason },
92
+ });
93
+
94
+ return true;
95
+ }
96
+
41
97
  /**
42
98
  * Random delay to prevent timing attacks (50-200ms)
43
99
  * Uses cryptographically secure random number generation
@@ -80,7 +136,7 @@ export class SessionService {
80
136
  realm: name,
81
137
  });
82
138
 
83
- await this.auditService.recordAuth("login_failed", {
139
+ await this.userAudits(userRealmName)?.recordAuth("login_failed", {
84
140
  userRealm: name,
85
141
  description: "Username does not match required format",
86
142
  metadata: { provider, username },
@@ -101,7 +157,7 @@ export class SessionService {
101
157
  realm: name,
102
158
  });
103
159
 
104
- await this.auditService.recordAuth("login_failed", {
160
+ await this.userAudits(userRealmName)?.recordAuth("login_failed", {
105
161
  userRealm: name,
106
162
  description: "Invalid login identifier format",
107
163
  metadata: { provider, username },
@@ -118,7 +174,7 @@ export class SessionService {
118
174
  realm: name,
119
175
  });
120
176
 
121
- await this.auditService.recordAuth("login_failed", {
177
+ await this.userAudits(userRealmName)?.recordAuth("login_failed", {
122
178
  userRealm: name,
123
179
  description: "User not found",
124
180
  metadata: { provider, username },
@@ -157,7 +213,7 @@ export class SessionService {
157
213
  realm: name,
158
214
  });
159
215
 
160
- await this.auditService.recordAuth("login_failed", {
216
+ await this.userAudits(userRealmName)?.recordAuth("login_failed", {
161
217
  userRealm: name,
162
218
  resourceId: user.id,
163
219
  description: "Invalid password",
@@ -167,7 +223,7 @@ export class SessionService {
167
223
  throw new InvalidCredentialsError();
168
224
  }
169
225
 
170
- await this.auditService.recordAuth("login", {
226
+ await this.userAudits(userRealmName)?.recordAuth("login", {
171
227
  userId: user.id,
172
228
  userEmail: user.email ?? undefined,
173
229
  userRealm: name,
@@ -176,6 +232,9 @@ export class SessionService {
176
232
  metadata: { provider, username },
177
233
  });
178
234
 
235
+ // Auto-promote to admin if configured
236
+ await this.ensureAdminRole(user, userRealmName);
237
+
179
238
  return user;
180
239
  } catch (error) {
181
240
  if (error instanceof InvalidCredentialsError) {
@@ -251,6 +310,9 @@ export class SessionService {
251
310
  },
252
311
  });
253
312
 
313
+ // Auto-promote to admin if configured (handles "I promote you admin" case)
314
+ await this.ensureAdminRole(user, userRealmName);
315
+
254
316
  this.log.debug("Session refreshed", {
255
317
  sessionId: session.id,
256
318
  userId: session.userId,
@@ -281,7 +343,7 @@ export class SessionService {
281
343
  if (session) {
282
344
  const { name } = this.realmProvider.getRealm(userRealmName);
283
345
 
284
- await this.auditService.recordAuth("logout", {
346
+ await this.userAudits(userRealmName)?.recordAuth("logout", {
285
347
  userId: session.userId,
286
348
  userRealm: name,
287
349
  sessionId: session.id,
@@ -324,7 +386,7 @@ export class SessionService {
324
386
 
325
387
  const user = await users.findById(identity.userId);
326
388
 
327
- await this.auditService.recordAuth("login", {
389
+ await this.userAudits(userRealmName)?.recordAuth("login", {
328
390
  userId: user.id,
329
391
  userEmail: user.email ?? undefined,
330
392
  userRealm: realm.name,
@@ -333,6 +395,9 @@ export class SessionService {
333
395
  metadata: { provider, providerUserId: profile.sub },
334
396
  });
335
397
 
398
+ // Auto-promote to admin if configured
399
+ await this.ensureAdminRole(user, userRealmName);
400
+
336
401
  return user;
337
402
  }
338
403
 
@@ -368,7 +433,7 @@ export class SessionService {
368
433
  userId: existing.id,
369
434
  });
370
435
 
371
- await this.auditService.recordAuth("login", {
436
+ await this.userAudits(userRealmName)?.recordAuth("login", {
372
437
  userId: existing.id,
373
438
  userEmail: existing.email ?? undefined,
374
439
  userRealm: realm.name,
@@ -377,6 +442,9 @@ export class SessionService {
377
442
  metadata: { provider, providerUserId: profile.sub, linked: true },
378
443
  });
379
444
 
445
+ // Auto-promote to admin if configured
446
+ await this.ensureAdminRole(existing, userRealmName);
447
+
380
448
  return existing;
381
449
  }
382
450
 
@@ -432,7 +500,7 @@ export class SessionService {
432
500
  });
433
501
 
434
502
  // Audit: user created via OAuth
435
- await this.auditService.recordUser("create", {
503
+ await this.userAudits(userRealmName)?.recordUser("create", {
436
504
  userId: user.id,
437
505
  userEmail: user.email ?? undefined,
438
506
  userRealm: realm.name,
@@ -447,7 +515,7 @@ export class SessionService {
447
515
  });
448
516
 
449
517
  // Audit: login event
450
- await this.auditService.recordAuth("login", {
518
+ await this.userAudits(userRealmName)?.recordAuth("login", {
451
519
  userId: user.id,
452
520
  userEmail: user.email ?? undefined,
453
521
  userRealm: realm.name,
@@ -456,6 +524,9 @@ export class SessionService {
456
524
  metadata: { provider, providerUserId: profile.sub, firstLogin: true },
457
525
  });
458
526
 
527
+ // Auto-promote to admin if configured
528
+ await this.ensureAdminRole(user, userRealmName);
529
+
459
530
  return user;
460
531
  }
461
532
  }
@@ -0,0 +1,47 @@
1
+ import { $inject } from "alepha";
2
+ import { AuditService, type CreateAudit } from "alepha/api/audits";
3
+
4
+ type AuditContext = Omit<CreateAudit, "type" | "action">;
5
+
6
+ /**
7
+ * User-specific audit wrapper service.
8
+ *
9
+ * This service wraps the core AuditService to provide user-related audit logging.
10
+ * It is lazy-loaded when the `audits` feature is enabled in the realm.
11
+ */
12
+ export class UserAudits {
13
+ protected readonly auditService = $inject(AuditService);
14
+
15
+ /**
16
+ * Record a user-related audit event.
17
+ */
18
+ public recordUser(
19
+ action:
20
+ | "create"
21
+ | "update"
22
+ | "delete"
23
+ | "role_change"
24
+ | "enable"
25
+ | "disable",
26
+ context: AuditContext,
27
+ ) {
28
+ return this.auditService.recordUser(action, context);
29
+ }
30
+
31
+ /**
32
+ * Record an authentication-related audit event.
33
+ */
34
+ public recordAuth(
35
+ action: "login" | "logout" | "login_failed" | "token_refresh",
36
+ context: AuditContext,
37
+ ) {
38
+ return this.auditService.recordAuth(action, context);
39
+ }
40
+
41
+ /**
42
+ * Record a generic audit event.
43
+ */
44
+ public record(category: string, action: string, context: AuditContext) {
45
+ return this.auditService.record(category, action, context);
46
+ }
47
+ }
@@ -0,0 +1,19 @@
1
+ import { $bucket } from "alepha/bucket";
2
+
3
+ /**
4
+ * User-specific file storage wrapper service.
5
+ *
6
+ * This service provides file storage for user-related files such as:
7
+ * - User avatars/profile pictures
8
+ *
9
+ * It is lazy-loaded when the `files` feature is enabled in the realm.
10
+ */
11
+ export class UserFiles {
12
+ /**
13
+ * Bucket for user avatar storage.
14
+ */
15
+ public readonly avatars = $bucket({
16
+ maxSize: 5 * 1024 * 1024, // 5 MB
17
+ mimeTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
18
+ });
19
+ }
@@ -0,0 +1,107 @@
1
+ import { Alepha } from "alepha";
2
+ import { AlephaApiJobs } from "alepha/api/jobs";
3
+ import { $repository, AlephaOrm } from "alepha/orm";
4
+ import { describe, test } from "vitest";
5
+ import { sessions } from "../entities/sessions.ts";
6
+ import { users } from "../entities/users.ts";
7
+ import { UserJobs } from "./UserJobs.ts";
8
+
9
+ describe("UserJobs", () => {
10
+ describe("purgeExpiredSessions", () => {
11
+ test("should delete expired sessions", async ({ expect }) => {
12
+ const alepha = Alepha.create().with(AlephaOrm).with(AlephaApiJobs);
13
+
14
+ class TestRepositories {
15
+ userRepository = $repository(users);
16
+ sessionRepository = $repository(sessions);
17
+ }
18
+
19
+ const userJobs = alepha.inject(UserJobs);
20
+ const repos = alepha.inject(TestRepositories);
21
+ await alepha.start();
22
+
23
+ // Create a test user
24
+ const user = await repos.userRepository.create({
25
+ email: "test@example.com",
26
+ });
27
+
28
+ // Create expired sessions (expiresAt in the past)
29
+ const pastDate = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); // 1 day ago
30
+ await repos.sessionRepository.create({
31
+ userId: user.id,
32
+ refreshToken: crypto.randomUUID(),
33
+ expiresAt: pastDate,
34
+ });
35
+ await repos.sessionRepository.create({
36
+ userId: user.id,
37
+ refreshToken: crypto.randomUUID(),
38
+ expiresAt: pastDate,
39
+ });
40
+
41
+ // Create a valid session (expiresAt in the future)
42
+ const futureDate = new Date(
43
+ Date.now() + 24 * 60 * 60 * 1000,
44
+ ).toISOString(); // 1 day from now
45
+ await repos.sessionRepository.create({
46
+ userId: user.id,
47
+ refreshToken: crypto.randomUUID(),
48
+ expiresAt: futureDate,
49
+ });
50
+
51
+ // Verify we have 3 sessions
52
+ const sessionsBefore = await repos.sessionRepository.findMany();
53
+ expect(sessionsBefore).toHaveLength(3);
54
+
55
+ // Trigger the job
56
+ await userJobs.purgeExpiredSessions.trigger();
57
+
58
+ // Verify only the valid session remains
59
+ const sessionsAfter = await repos.sessionRepository.findMany();
60
+ expect(sessionsAfter).toHaveLength(1);
61
+ expect(new Date(sessionsAfter[0].expiresAt).getTime()).toBeGreaterThan(
62
+ Date.now(),
63
+ );
64
+ });
65
+
66
+ test("should handle case when no expired sessions exist", async ({
67
+ expect,
68
+ }) => {
69
+ const alepha = Alepha.create().with(AlephaOrm).with(AlephaApiJobs);
70
+
71
+ class TestRepositories {
72
+ userRepository = $repository(users);
73
+ sessionRepository = $repository(sessions);
74
+ }
75
+
76
+ const userJobs = alepha.inject(UserJobs);
77
+ const repos = alepha.inject(TestRepositories);
78
+ await alepha.start();
79
+
80
+ // Create a test user
81
+ const user = await repos.userRepository.create({
82
+ email: "test2@example.com",
83
+ });
84
+
85
+ // Create only valid sessions
86
+ const futureDate = new Date(
87
+ Date.now() + 24 * 60 * 60 * 1000,
88
+ ).toISOString();
89
+ await repos.sessionRepository.create({
90
+ userId: user.id,
91
+ refreshToken: crypto.randomUUID(),
92
+ expiresAt: futureDate,
93
+ });
94
+
95
+ // Verify we have 1 session
96
+ const sessionsBefore = await repos.sessionRepository.findMany();
97
+ expect(sessionsBefore).toHaveLength(1);
98
+
99
+ // Trigger the job - should not throw
100
+ await userJobs.purgeExpiredSessions.trigger();
101
+
102
+ // Session should still exist
103
+ const sessionsAfter = await repos.sessionRepository.findMany();
104
+ expect(sessionsAfter).toHaveLength(1);
105
+ });
106
+ });
107
+ });
@@ -0,0 +1,62 @@
1
+ import { $inject } from "alepha";
2
+ import { $job } from "alepha/api/jobs";
3
+ import { DateTimeProvider } from "alepha/datetime";
4
+ import { $logger } from "alepha/logger";
5
+ import { $repository } from "alepha/orm";
6
+ import { sessions } from "../entities/sessions.ts";
7
+
8
+ /**
9
+ * User-specific jobs wrapper service.
10
+ *
11
+ * This service handles user-related scheduled jobs such as:
12
+ * - Session purge (cleaning up expired sessions)
13
+ * - Verification code cleanup
14
+ * - Inactive user notifications
15
+ *
16
+ * It is lazy-loaded when the `jobs` feature is enabled in the realm.
17
+ */
18
+ export class UserJobs {
19
+ protected readonly log = $logger();
20
+ protected readonly dateTimeProvider = $inject(DateTimeProvider);
21
+ protected readonly sessionRepository = $repository(sessions);
22
+
23
+ /**
24
+ * Purge expired sessions from the database.
25
+ *
26
+ * This job runs daily at 3:00 AM and removes all sessions
27
+ * where the `expiresAt` timestamp has passed.
28
+ */
29
+ public readonly purgeExpiredSessions = $job({
30
+ name: "users.purgeExpiredSessions",
31
+ description: "Remove expired user sessions from the database",
32
+ cron: "0 3 * * *", // Daily at 3:00 AM
33
+ handler: async () => {
34
+ const now = this.dateTimeProvider.nowISOString();
35
+
36
+ this.log.info("Starting expired sessions purge", { cutoffTime: now });
37
+
38
+ const expiredSessions = await this.sessionRepository.findMany({
39
+ where: {
40
+ expiresAt: { lt: now },
41
+ },
42
+ });
43
+
44
+ if (expiredSessions.length === 0) {
45
+ this.log.info("No expired sessions found");
46
+ return;
47
+ }
48
+
49
+ this.log.info("Found expired sessions", {
50
+ count: expiredSessions.length,
51
+ });
52
+
53
+ const deletedIds = await this.sessionRepository.deleteMany({
54
+ expiresAt: { lt: now },
55
+ });
56
+
57
+ this.log.info("Expired sessions purged successfully", {
58
+ deletedCount: deletedIds.length,
59
+ });
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,23 @@
1
+ import { $config } from "alepha/api/parameters";
2
+ import { realmAuthSettingsAtom } from "../atoms/realmAuthSettingsAtom.ts";
3
+
4
+ /**
5
+ * User-specific configuration service.
6
+ *
7
+ * This service wraps the core ConfigStore to provide realm settings management.
8
+ * It is lazy-loaded when the `parameters` feature is enabled in the realm.
9
+ */
10
+ export class UserParameters {
11
+ /**
12
+ * Realm authentication settings configuration.
13
+ *
14
+ * Controls user registration, login methods, verification requirements,
15
+ * and password policies for the realm.
16
+ */
17
+ public readonly realmSettings = $config({
18
+ name: "alepha.api.users.realmSettings",
19
+ description: "Realm authentication and registration settings",
20
+ schema: realmAuthSettingsAtom.schema,
21
+ default: realmAuthSettingsAtom.options.default,
22
+ });
23
+ }
@@ -1,23 +1,38 @@
1
- import { $inject } from "alepha";
2
- import { AuditService } from "alepha/api/audits";
1
+ import { $inject, Alepha } from "alepha";
3
2
  import type { VerificationController } from "alepha/api/verifications";
4
3
  import { $logger } from "alepha/logger";
5
4
  import { type Page, parseQueryString } from "alepha/orm";
6
5
  import { BadRequestError } from "alepha/server";
7
6
  import { $client } from "alepha/server/links";
8
7
  import type { UserEntity } from "../entities/users.ts";
9
- import { UserNotifications } from "../notifications/UserNotifications.ts";
10
8
  import { RealmProvider } from "../providers/RealmProvider.ts";
11
9
  import type { CreateUser } from "../schemas/createUserSchema.ts";
12
10
  import type { UpdateUser } from "../schemas/updateUserSchema.ts";
13
11
  import type { UserQuery } from "../schemas/userQuerySchema.ts";
12
+ import { UserAudits } from "./UserAudits.ts";
13
+ import { UserNotifications } from "./UserNotifications.ts";
14
14
 
15
15
  export class UserService {
16
+ protected readonly alepha = $inject(Alepha);
16
17
  protected readonly log = $logger();
17
18
  protected readonly verificationController = $client<VerificationController>();
18
- protected readonly userNotifications = $inject(UserNotifications);
19
19
  protected readonly realmProvider = $inject(RealmProvider);
20
- protected readonly auditService = $inject(AuditService);
20
+
21
+ protected userAudits(realmName?: string) {
22
+ const realm = this.realmProvider.getRealm(realmName);
23
+ if (realm.features.audits) {
24
+ return this.alepha.inject(UserAudits);
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ protected userNotifications(realmName?: string) {
30
+ const realm = this.realmProvider.getRealm(realmName);
31
+ if (realm.features.notifications) {
32
+ return this.alepha.inject(UserNotifications);
33
+ }
34
+ return undefined;
35
+ }
21
36
 
22
37
  public users(userRealmName?: string) {
23
38
  return this.realmProvider.userRepository(userRealmName);
@@ -79,21 +94,23 @@ export class UserService {
79
94
  ? `${verifyUrl}${url.search}`
80
95
  : url.pathname + url.search;
81
96
 
82
- await this.userNotifications.emailVerificationLink.push({
83
- contact: email,
84
- variables: {
85
- email,
86
- verifyUrl: fullVerifyUrl,
87
- expiresInMinutes: Math.floor(verification.codeExpiration / 60),
97
+ await this.userNotifications(userRealmName)?.emailVerificationLink.push(
98
+ {
99
+ contact: email,
100
+ variables: {
101
+ email,
102
+ verifyUrl: fullVerifyUrl,
103
+ expiresInMinutes: Math.floor(verification.codeExpiration / 60),
104
+ },
88
105
  },
89
- });
106
+ );
90
107
 
91
108
  this.log.debug("Email verification link sent", {
92
109
  email,
93
110
  userId: user.id,
94
111
  });
95
112
  } else {
96
- await this.userNotifications.emailVerification.push({
113
+ await this.userNotifications(userRealmName)?.emailVerification.push({
97
114
  contact: email,
98
115
  variables: {
99
116
  email,
@@ -158,7 +175,7 @@ export class UserService {
158
175
 
159
176
  const realm = this.realmProvider.getRealm(userRealmName);
160
177
 
161
- await this.auditService.recordUser("update", {
178
+ await this.userAudits(userRealmName)?.recordUser("update", {
162
179
  userId: user.id,
163
180
  userEmail: email,
164
181
  userRealm: realm.name,
@@ -314,7 +331,7 @@ export class UserService {
314
331
  email: user.email,
315
332
  });
316
333
 
317
- await this.auditService.recordUser("create", {
334
+ await this.userAudits(userRealmName)?.recordUser("create", {
318
335
  userRealm: realm.name,
319
336
  resourceId: user.id,
320
337
  description: "User created",
@@ -357,7 +374,7 @@ export class UserService {
357
374
  data.roles !== undefined &&
358
375
  JSON.stringify(before.roles) !== JSON.stringify(data.roles);
359
376
 
360
- await this.auditService.recordUser(
377
+ await this.userAudits(userRealmName)?.recordUser(
361
378
  isRoleChange ? "role_change" : "update",
362
379
  {
363
380
  userRealm: realm.name,
@@ -384,7 +401,7 @@ export class UserService {
384
401
 
385
402
  const realm = this.realmProvider.getRealm(userRealmName);
386
403
 
387
- await this.auditService.recordUser("delete", {
404
+ await this.userAudits(userRealmName)?.recordUser("delete", {
388
405
  userRealm: realm.name,
389
406
  resourceId: id,
390
407
  severity: "warning",
@@ -18,9 +18,9 @@ export * from "./services/VerificationService.ts";
18
18
  // ---------------------------------------------------------------------------------------------------------------------
19
19
 
20
20
  /**
21
- * | type | quality | stability |
22
- * |------|---------|-----------|
23
- * | backend | standard | stable |
21
+ * | Stability | Since | Runtime |
22
+ * |-----------|-------|---------|
23
+ * | 3 - stable | 0.13.0 | node, bun, workerd|
24
24
  *
25
25
  * Email and phone verification workflows.
26
26
  *
@@ -10,9 +10,9 @@ export * from "./providers/BatchProvider.ts";
10
10
  // ---------------------------------------------------------------------------------------------------------------------
11
11
 
12
12
  /**
13
- * | type | quality | stability |
14
- * |------|---------|-----------|
15
- * | backend | standard | stable |
13
+ * | Stability | Since | Runtime |
14
+ * |-----------|-------|---------|
15
+ * | 3 - stable | 0.8.0 | node, bun|
16
16
  *
17
17
  * Batch accumulation and processing.
18
18
  *
@@ -43,9 +43,9 @@ declare module "alepha" {
43
43
  // ---------------------------------------------------------------------------------------------------------------------
44
44
 
45
45
  /**
46
- * | type | quality | stability |
47
- * |------|---------|-----------|
48
- * | backend | rare | stable |
46
+ * | Stability | Since | Runtime |
47
+ * |-----------|-------|---------|
48
+ * | 3 - stable | 0.9.0 | node, bun, workerd|
49
49
  *
50
50
  * Unified file storage abstraction across multiple backends.
51
51
  *
@@ -12,9 +12,9 @@ export * from "./providers/MemoryCacheProvider.ts";
12
12
  // ---------------------------------------------------------------------------------------------------------------------
13
13
 
14
14
  /**
15
- * | type | quality | stability |
16
- * |------|---------|-----------|
17
- * | backend | rare | stable |
15
+ * | Stability | Since | Runtime |
16
+ * |-----------|-------|---------|
17
+ * | 3 - stable | 0.9.0 | node, bun, workerd|
18
18
  *
19
19
  * Type-safe caching with TTL support.
20
20
  *
@@ -269,6 +269,7 @@ export class BuildCommand {
269
269
  generateCloudflare({
270
270
  distDir,
271
271
  config: options.cloudflare?.config,
272
+ alepha: alepha!,
272
273
  }),
273
274
  });
274
275
  }