alepha 0.15.4 → 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 (282) hide show
  1. package/README.md +26 -11
  2. package/dist/api/audits/index.d.ts +3 -3
  3. package/dist/api/audits/index.js +3 -3
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.d.ts +3 -3
  6. package/dist/api/files/index.js +3 -3
  7. package/dist/api/files/index.js.map +1 -1
  8. package/dist/api/jobs/index.d.ts +47 -4
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/jobs/index.js +100 -5
  11. package/dist/api/jobs/index.js.map +1 -1
  12. package/dist/api/keys/index.d.ts +3 -3
  13. package/dist/api/keys/index.js +3 -3
  14. package/dist/api/keys/index.js.map +1 -1
  15. package/dist/api/notifications/index.d.ts +3 -3
  16. package/dist/api/notifications/index.js +3 -3
  17. package/dist/api/notifications/index.js.map +1 -1
  18. package/dist/api/parameters/index.d.ts +263 -263
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/parameters/index.js +31 -30
  21. package/dist/api/parameters/index.js.map +1 -1
  22. package/dist/api/users/index.d.ts +373 -67
  23. package/dist/api/users/index.d.ts.map +1 -1
  24. package/dist/api/users/index.js +273 -72
  25. package/dist/api/users/index.js.map +1 -1
  26. package/dist/api/verifications/index.d.ts +3 -3
  27. package/dist/api/verifications/index.js +3 -3
  28. package/dist/api/verifications/index.js.map +1 -1
  29. package/dist/batch/index.d.ts +7 -7
  30. package/dist/batch/index.js +3 -3
  31. package/dist/batch/index.js.map +1 -1
  32. package/dist/bucket/index.d.ts +3 -3
  33. package/dist/bucket/index.js +6 -6
  34. package/dist/bucket/index.js.map +1 -1
  35. package/dist/cache/core/index.d.ts +3 -3
  36. package/dist/cache/core/index.js +3 -3
  37. package/dist/cache/core/index.js.map +1 -1
  38. package/dist/cli/index.d.ts +5607 -20
  39. package/dist/cli/index.d.ts.map +1 -1
  40. package/dist/cli/index.js +103 -89
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/command/index.d.ts +11 -4
  43. package/dist/command/index.d.ts.map +1 -1
  44. package/dist/command/index.js +8 -6
  45. package/dist/command/index.js.map +1 -1
  46. package/dist/core/index.browser.js.map +1 -1
  47. package/dist/core/index.d.ts +4 -8
  48. package/dist/core/index.d.ts.map +1 -1
  49. package/dist/core/index.js +3 -3
  50. package/dist/core/index.js.map +1 -1
  51. package/dist/core/index.native.js.map +1 -1
  52. package/dist/datetime/index.d.ts +3 -3
  53. package/dist/datetime/index.js +3 -3
  54. package/dist/datetime/index.js.map +1 -1
  55. package/dist/email/index.d.ts +3 -3
  56. package/dist/email/index.js +8 -8
  57. package/dist/email/index.js.map +1 -1
  58. package/dist/fake/index.d.ts +3 -3
  59. package/dist/fake/index.js +3 -3
  60. package/dist/fake/index.js.map +1 -1
  61. package/dist/lock/core/index.d.ts +3 -3
  62. package/dist/lock/core/index.js +3 -3
  63. package/dist/lock/core/index.js.map +1 -1
  64. package/dist/logger/index.d.ts +3 -3
  65. package/dist/logger/index.js +6 -3
  66. package/dist/logger/index.js.map +1 -1
  67. package/dist/mcp/index.d.ts +3 -3
  68. package/dist/mcp/index.js +3 -3
  69. package/dist/mcp/index.js.map +1 -1
  70. package/dist/orm/index.d.ts +12 -12
  71. package/dist/orm/index.js +4 -4
  72. package/dist/orm/index.js.map +1 -1
  73. package/dist/queue/core/index.d.ts +3 -3
  74. package/dist/queue/core/index.js +3 -3
  75. package/dist/queue/core/index.js.map +1 -1
  76. package/dist/react/auth/index.d.ts +3 -3
  77. package/dist/react/auth/index.js +3 -3
  78. package/dist/react/auth/index.js.map +1 -1
  79. package/dist/react/core/index.d.ts +3 -3
  80. package/dist/react/core/index.js +3 -3
  81. package/dist/react/core/index.js.map +1 -1
  82. package/dist/react/form/index.d.ts +3 -3
  83. package/dist/react/form/index.js +3 -3
  84. package/dist/react/form/index.js.map +1 -1
  85. package/dist/react/head/index.d.ts +3 -3
  86. package/dist/react/head/index.js +3 -3
  87. package/dist/react/head/index.js.map +1 -1
  88. package/dist/react/i18n/index.d.ts +3 -3
  89. package/dist/react/i18n/index.js +3 -3
  90. package/dist/react/i18n/index.js.map +1 -1
  91. package/dist/react/intro/index.css +337 -0
  92. package/dist/react/intro/index.css.map +1 -0
  93. package/dist/react/intro/index.d.ts +10 -0
  94. package/dist/react/intro/index.d.ts.map +1 -0
  95. package/dist/react/intro/index.js +222 -0
  96. package/dist/react/intro/index.js.map +1 -0
  97. package/dist/react/router/index.browser.js +2 -2
  98. package/dist/react/router/index.browser.js.map +1 -1
  99. package/dist/react/router/index.d.ts +1 -1
  100. package/dist/react/router/index.d.ts.map +1 -1
  101. package/dist/react/router/index.js +5 -5
  102. package/dist/react/router/index.js.map +1 -1
  103. package/dist/redis/index.d.ts +17 -17
  104. package/dist/redis/index.js +3 -3
  105. package/dist/redis/index.js.map +1 -1
  106. package/dist/retry/index.d.ts +3 -3
  107. package/dist/retry/index.js +3 -3
  108. package/dist/retry/index.js.map +1 -1
  109. package/dist/scheduler/index.d.ts +3 -3
  110. package/dist/scheduler/index.js +3 -3
  111. package/dist/scheduler/index.js.map +1 -1
  112. package/dist/security/index.d.ts +3 -3
  113. package/dist/security/index.js +5 -5
  114. package/dist/security/index.js.map +1 -1
  115. package/dist/server/auth/index.d.ts +3 -3
  116. package/dist/server/auth/index.js +3 -3
  117. package/dist/server/auth/index.js.map +1 -1
  118. package/dist/server/cache/index.d.ts +3 -3
  119. package/dist/server/cache/index.js +3 -3
  120. package/dist/server/cache/index.js.map +1 -1
  121. package/dist/server/compress/index.d.ts +3 -3
  122. package/dist/server/compress/index.js +3 -3
  123. package/dist/server/compress/index.js.map +1 -1
  124. package/dist/server/cookies/index.d.ts +3 -3
  125. package/dist/server/cookies/index.js +3 -3
  126. package/dist/server/cookies/index.js.map +1 -1
  127. package/dist/server/core/index.d.ts +5 -16
  128. package/dist/server/core/index.d.ts.map +1 -1
  129. package/dist/server/core/index.js +13 -29
  130. package/dist/server/core/index.js.map +1 -1
  131. package/dist/server/cors/index.d.ts +3 -3
  132. package/dist/server/cors/index.js +3 -3
  133. package/dist/server/cors/index.js.map +1 -1
  134. package/dist/server/health/index.d.ts +20 -20
  135. package/dist/server/health/index.js +3 -3
  136. package/dist/server/health/index.js.map +1 -1
  137. package/dist/server/helmet/index.d.ts +3 -3
  138. package/dist/server/helmet/index.js +3 -3
  139. package/dist/server/helmet/index.js.map +1 -1
  140. package/dist/server/links/index.d.ts +42 -42
  141. package/dist/server/links/index.d.ts.map +1 -1
  142. package/dist/server/links/index.js +3 -3
  143. package/dist/server/links/index.js.map +1 -1
  144. package/dist/server/metrics/index.d.ts +3 -3
  145. package/dist/server/metrics/index.js +3 -3
  146. package/dist/server/metrics/index.js.map +1 -1
  147. package/dist/server/multipart/index.d.ts +3 -3
  148. package/dist/server/multipart/index.js +3 -3
  149. package/dist/server/multipart/index.js.map +1 -1
  150. package/dist/server/proxy/index.d.ts +3 -3
  151. package/dist/server/proxy/index.js +3 -3
  152. package/dist/server/proxy/index.js.map +1 -1
  153. package/dist/server/rate-limit/index.d.ts +3 -3
  154. package/dist/server/rate-limit/index.js +3 -3
  155. package/dist/server/rate-limit/index.js.map +1 -1
  156. package/dist/server/static/index.d.ts +3 -3
  157. package/dist/server/static/index.js +6 -6
  158. package/dist/server/static/index.js.map +1 -1
  159. package/dist/server/swagger/index.d.ts +3 -3
  160. package/dist/server/swagger/index.js +6 -6
  161. package/dist/server/swagger/index.js.map +1 -1
  162. package/dist/sms/index.d.ts +3 -3
  163. package/dist/sms/index.js +6 -6
  164. package/dist/sms/index.js.map +1 -1
  165. package/dist/system/index.d.ts +3 -3
  166. package/dist/system/index.js +3 -3
  167. package/dist/system/index.js.map +1 -1
  168. package/dist/thread/index.d.ts +3 -3
  169. package/dist/thread/index.js +3 -3
  170. package/dist/thread/index.js.map +1 -1
  171. package/dist/topic/core/index.d.ts +3 -3
  172. package/dist/topic/core/index.js +3 -3
  173. package/dist/topic/core/index.js.map +1 -1
  174. package/dist/vite/index.d.ts +6284 -3
  175. package/dist/vite/index.d.ts.map +1 -1
  176. package/dist/websocket/index.d.ts +3 -3
  177. package/dist/websocket/index.js +3 -3
  178. package/dist/websocket/index.js.map +1 -1
  179. package/package.json +7 -2
  180. package/src/api/audits/index.ts +3 -3
  181. package/src/api/files/index.ts +3 -3
  182. package/src/api/jobs/controllers/AdminJobController.ts +15 -2
  183. package/src/api/jobs/index.ts +4 -3
  184. package/src/api/jobs/services/JobAudits.spec.ts +89 -0
  185. package/src/api/jobs/services/JobAudits.ts +101 -0
  186. package/src/api/keys/index.ts +3 -3
  187. package/src/api/notifications/index.ts +3 -3
  188. package/src/api/parameters/index.ts +5 -3
  189. package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1 -1
  190. package/src/api/users/__tests__/ApiKeys.spec.ts +1 -1
  191. package/src/api/users/__tests__/EmailVerification.spec.ts +16 -1
  192. package/src/api/users/__tests__/PasswordReset.spec.ts +11 -0
  193. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -0
  194. package/src/api/users/index.ts +8 -9
  195. package/src/api/users/primitives/$realm.ts +117 -19
  196. package/src/api/users/providers/RealmProvider.ts +15 -7
  197. package/src/api/users/services/CredentialService.spec.ts +11 -0
  198. package/src/api/users/services/CredentialService.ts +47 -24
  199. package/src/api/users/services/IdentityService.ts +12 -4
  200. package/src/api/users/services/RegistrationService.spec.ts +11 -0
  201. package/src/api/users/services/RegistrationService.ts +33 -12
  202. package/src/api/users/services/SessionService.ts +83 -12
  203. package/src/api/users/services/UserAudits.ts +47 -0
  204. package/src/api/users/services/UserFiles.ts +19 -0
  205. package/src/api/users/services/UserJobs.spec.ts +107 -0
  206. package/src/api/users/services/UserJobs.ts +62 -0
  207. package/src/api/users/services/UserParameters.ts +23 -0
  208. package/src/api/users/services/UserService.ts +34 -17
  209. package/src/api/verifications/index.ts +3 -3
  210. package/src/batch/index.ts +3 -3
  211. package/src/bucket/index.ts +3 -3
  212. package/src/cache/core/index.ts +3 -3
  213. package/src/cli/commands/db.ts +9 -0
  214. package/src/cli/commands/init.spec.ts +2 -17
  215. package/src/cli/commands/init.ts +37 -1
  216. package/src/cli/providers/ViteDevServerProvider.ts +5 -2
  217. package/src/cli/services/AlephaCliUtils.ts +17 -0
  218. package/src/cli/services/PackageManagerUtils.ts +15 -1
  219. package/src/cli/services/ProjectScaffolder.ts +8 -13
  220. package/src/cli/templates/agentMd.ts +2 -25
  221. package/src/cli/templates/apiAppSecurityTs.ts +37 -2
  222. package/src/cli/templates/mainCss.ts +2 -32
  223. package/src/cli/templates/webAppRouterTs.ts +5 -5
  224. package/src/cli/templates/webHomeComponentTsx.ts +10 -0
  225. package/src/command/helpers/Runner.ts +14 -1
  226. package/src/command/index.ts +3 -3
  227. package/src/core/helpers/primitive.ts +0 -5
  228. package/src/core/index.ts +3 -3
  229. package/src/datetime/index.ts +3 -3
  230. package/src/email/index.ts +3 -3
  231. package/src/email/providers/LocalEmailProvider.ts +2 -2
  232. package/src/fake/index.ts +3 -3
  233. package/src/lock/core/index.ts +3 -3
  234. package/src/logger/index.ts +3 -3
  235. package/src/logger/providers/PrettyFormatterProvider.ts +7 -0
  236. package/src/mcp/index.ts +3 -3
  237. package/src/orm/index.ts +3 -3
  238. package/src/orm/providers/drivers/NodeSqliteProvider.ts +1 -1
  239. package/src/queue/core/index.ts +3 -3
  240. package/src/react/auth/index.ts +3 -3
  241. package/src/react/core/index.ts +3 -3
  242. package/src/react/form/index.ts +3 -3
  243. package/src/react/head/index.ts +3 -3
  244. package/src/react/i18n/index.ts +3 -3
  245. package/src/react/intro/components/GettingStarted.css +334 -0
  246. package/src/react/intro/components/GettingStarted.tsx +276 -0
  247. package/src/react/intro/index.ts +1 -0
  248. package/src/react/router/index.browser.ts +2 -0
  249. package/src/react/router/index.ts +2 -0
  250. package/src/redis/index.ts +3 -3
  251. package/src/retry/index.ts +3 -3
  252. package/src/router/index.ts +3 -3
  253. package/src/scheduler/index.ts +3 -3
  254. package/src/security/index.ts +3 -3
  255. package/src/security/providers/JwtProvider.ts +2 -2
  256. package/src/server/auth/index.ts +3 -3
  257. package/src/server/cache/index.ts +3 -3
  258. package/src/server/compress/index.ts +3 -3
  259. package/src/server/cookies/index.ts +3 -3
  260. package/src/server/core/index.ts +3 -3
  261. package/src/server/core/primitives/$action.spec.ts +3 -2
  262. package/src/server/core/primitives/$action.ts +6 -2
  263. package/src/server/core/providers/NodeHttpServerProvider.ts +2 -15
  264. package/src/server/core/providers/ServerProvider.ts +4 -2
  265. package/src/server/core/providers/ServerRouterProvider.ts +5 -27
  266. package/src/server/cors/index.ts +3 -3
  267. package/src/server/health/index.ts +3 -3
  268. package/src/server/helmet/index.ts +3 -3
  269. package/src/server/links/index.ts +3 -3
  270. package/src/server/metrics/index.ts +3 -3
  271. package/src/server/multipart/index.ts +3 -3
  272. package/src/server/proxy/index.ts +3 -3
  273. package/src/server/rate-limit/index.ts +3 -3
  274. package/src/server/static/index.ts +3 -3
  275. package/src/server/swagger/index.ts +3 -3
  276. package/src/sms/index.ts +3 -3
  277. package/src/system/index.ts +3 -3
  278. package/src/thread/index.ts +3 -3
  279. package/src/topic/core/index.ts +3 -3
  280. package/src/websocket/index.ts +3 -3
  281. package/src/cli/templates/webHelloComponentTsx.ts +0 -30
  282. /package/src/api/users/{notifications → services}/UserNotifications.ts +0 -0
@@ -1,6 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { $inject } from "alepha";
3
- import { AuditService } from "alepha/api/audits";
2
+ import { $inject, Alepha } from "alepha";
4
3
  import type { VerificationController } from "alepha/api/verifications";
5
4
  import { $cache } from "alepha/cache";
6
5
  import { DateTimeProvider } from "alepha/datetime";
@@ -8,10 +7,11 @@ import { $logger } from "alepha/logger";
8
7
  import { CryptoProvider } from "alepha/security";
9
8
  import { BadRequestError, HttpError } from "alepha/server";
10
9
  import { $client } from "alepha/server/links";
11
- import { UserNotifications } from "../notifications/UserNotifications.ts";
12
10
  import { RealmProvider } from "../providers/RealmProvider.ts";
13
11
  import type { CompletePasswordResetRequest } from "../schemas/completePasswordResetRequestSchema.ts";
14
12
  import type { PasswordResetIntentResponse } from "../schemas/passwordResetIntentResponseSchema.ts";
13
+ import { UserAudits } from "./UserAudits.ts";
14
+ import { UserNotifications } from "./UserNotifications.ts";
15
15
 
16
16
  /**
17
17
  * Intent stored in cache during the password reset flow.
@@ -27,13 +27,28 @@ interface PasswordResetIntent {
27
27
  const INTENT_TTL_MINUTES = 10;
28
28
 
29
29
  export class CredentialService {
30
+ protected readonly alepha = $inject(Alepha);
30
31
  protected readonly log = $logger();
31
32
  protected readonly cryptoProvider = $inject(CryptoProvider);
32
33
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
33
34
  protected readonly verificationController = $client<VerificationController>();
34
- protected readonly userNotifications = $inject(UserNotifications);
35
35
  protected readonly realmProvider = $inject(RealmProvider);
36
- protected readonly auditService = $inject(AuditService);
36
+
37
+ protected userAudits(realmName?: string) {
38
+ const realm = this.realmProvider.getRealm(realmName);
39
+ if (realm.features.audits) {
40
+ return this.alepha.inject(UserAudits);
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ protected userNotifications(realmName?: string) {
46
+ const realm = this.realmProvider.getRealm(realmName);
47
+ if (realm.features.notifications) {
48
+ return this.alepha.inject(UserNotifications);
49
+ }
50
+ return undefined;
51
+ }
37
52
 
38
53
  protected readonly intentCache = $cache<PasswordResetIntent>({
39
54
  name: "password-reset-intents",
@@ -118,7 +133,7 @@ export class CredentialService {
118
133
  });
119
134
 
120
135
  // Send password reset notification with the code
121
- await this.userNotifications.passwordReset.push({
136
+ await this.userNotifications(userRealmName)?.passwordReset.push({
122
137
  contact: email,
123
138
  variables: {
124
139
  email,
@@ -225,7 +240,7 @@ export class CredentialService {
225
240
  const realm = this.realmProvider.getRealm(intent.realmName);
226
241
 
227
242
  // Audit: password reset
228
- await this.auditService.recordUser("update", {
243
+ await this.userAudits(intent.realmName)?.recordUser("update", {
229
244
  userId: intent.userId,
230
245
  userEmail: intent.email,
231
246
  userRealm: realm.name,
@@ -235,14 +250,18 @@ export class CredentialService {
235
250
  });
236
251
 
237
252
  // Audit: sessions invalidated (security event)
238
- await this.auditService.record("security", "sessions_invalidated", {
239
- userId: intent.userId,
240
- userEmail: intent.email,
241
- userRealm: realm.name,
242
- resourceId: intent.userId,
243
- severity: "warning",
244
- description: "All sessions invalidated after password reset",
245
- });
253
+ await this.userAudits(intent.realmName)?.record(
254
+ "security",
255
+ "sessions_invalidated",
256
+ {
257
+ userId: intent.userId,
258
+ userEmail: intent.email,
259
+ userRealm: realm.name,
260
+ resourceId: intent.userId,
261
+ severity: "warning",
262
+ description: "All sessions invalidated after password reset",
263
+ },
264
+ );
246
265
  }
247
266
 
248
267
  // Legacy methods kept for backward compatibility
@@ -333,7 +352,7 @@ export class CredentialService {
333
352
  const realm = this.realmProvider.getRealm(userRealmName);
334
353
 
335
354
  // Audit: password reset (legacy method)
336
- await this.auditService.recordUser("update", {
355
+ await this.userAudits(userRealmName)?.recordUser("update", {
337
356
  userId: user.id,
338
357
  userEmail: email,
339
358
  userRealm: realm.name,
@@ -343,13 +362,17 @@ export class CredentialService {
343
362
  });
344
363
 
345
364
  // Audit: sessions invalidated
346
- await this.auditService.record("security", "sessions_invalidated", {
347
- userId: user.id,
348
- userEmail: email,
349
- userRealm: realm.name,
350
- resourceId: user.id,
351
- severity: "warning",
352
- description: "All sessions invalidated after password reset",
353
- });
365
+ await this.userAudits(userRealmName)?.record(
366
+ "security",
367
+ "sessions_invalidated",
368
+ {
369
+ userId: user.id,
370
+ userEmail: email,
371
+ userRealm: realm.name,
372
+ resourceId: user.id,
373
+ severity: "warning",
374
+ description: "All sessions invalidated after password reset",
375
+ },
376
+ );
354
377
  }
355
378
  }
@@ -1,15 +1,23 @@
1
- import { $inject } from "alepha";
2
- import { AuditService } from "alepha/api/audits";
1
+ import { $inject, Alepha } from "alepha";
3
2
  import { $logger } from "alepha/logger";
4
3
  import type { Page } from "alepha/orm";
5
4
  import type { IdentityEntity } from "../entities/identities.ts";
6
5
  import { RealmProvider } from "../providers/RealmProvider.ts";
7
6
  import type { IdentityQuery } from "../schemas/identityQuerySchema.ts";
7
+ import { UserAudits } from "./UserAudits.ts";
8
8
 
9
9
  export class IdentityService {
10
+ protected readonly alepha = $inject(Alepha);
10
11
  protected readonly log = $logger();
11
12
  protected readonly realmProvider = $inject(RealmProvider);
12
- protected readonly auditService = $inject(AuditService);
13
+
14
+ protected userAudits(realmName?: string) {
15
+ const realm = this.realmProvider.getRealm(realmName);
16
+ if (realm.features.audits) {
17
+ return this.alepha.inject(UserAudits);
18
+ }
19
+ return undefined;
20
+ }
13
21
 
14
22
  public identities(userRealmName?: string) {
15
23
  return this.realmProvider.identityRepository(userRealmName);
@@ -87,7 +95,7 @@ export class IdentityService {
87
95
 
88
96
  const realm = this.realmProvider.getRealm(userRealmName);
89
97
 
90
- await this.auditService.recordUser("update", {
98
+ await this.userAudits(userRealmName)?.recordUser("update", {
91
99
  userRealm: realm.name,
92
100
  resourceId: identity.userId,
93
101
  description: `Identity provider disconnected: ${identity.provider}`,
@@ -10,6 +10,7 @@ import {
10
10
  RealmProvider,
11
11
  RegistrationService,
12
12
  SessionService,
13
+ UserNotifications,
13
14
  UserService,
14
15
  } from "../index.ts";
15
16
 
@@ -20,6 +21,7 @@ const setup = async (realmSettings?: Record<string, unknown>) => {
20
21
  alepha.with(AlephaEmail);
21
22
  alepha.with(AlephaApiVerification);
22
23
  alepha.with(AlephaApiUsers);
24
+ alepha.with(UserNotifications);
23
25
 
24
26
  await alepha.start();
25
27
 
@@ -84,6 +86,9 @@ describe("alepha/api/users - RegistrationService", () => {
84
86
 
85
87
  // Register realm with email verification required
86
88
  realmProvider.register("verify-email-realm", {
89
+ features: {
90
+ notifications: true,
91
+ },
87
92
  settings: {
88
93
  verifyEmailRequired: true,
89
94
  } as never,
@@ -315,6 +320,9 @@ describe("alepha/api/users - RegistrationService", () => {
315
320
  await setup();
316
321
 
317
322
  realmProvider.register("email-verify-realm", {
323
+ features: {
324
+ notifications: true,
325
+ },
318
326
  settings: {
319
327
  verifyEmailRequired: true,
320
328
  } as never,
@@ -586,6 +594,9 @@ describe("alepha/api/users - RegistrationService", () => {
586
594
  } = await setup();
587
595
 
588
596
  realmProvider.register("full-verify-realm", {
597
+ features: {
598
+ notifications: true,
599
+ },
589
600
  settings: {
590
601
  verifyEmailRequired: true,
591
602
  } as never,
@@ -1,6 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { $inject } from "alepha";
3
- import { AuditService } from "alepha/api/audits";
2
+ import { $inject, Alepha } from "alepha";
4
3
  import type { VerificationController } from "alepha/api/verifications";
5
4
  import { $cache } from "alepha/cache";
6
5
  import { DateTimeProvider } from "alepha/datetime";
@@ -9,11 +8,12 @@ import { CryptoProvider } from "alepha/security";
9
8
  import { BadRequestError, ConflictError, HttpError } from "alepha/server";
10
9
  import { $client } from "alepha/server/links";
11
10
  import type { UserEntity } from "../entities/users.ts";
12
- import { UserNotifications } from "../notifications/UserNotifications.ts";
13
11
  import { RealmProvider } from "../providers/RealmProvider.ts";
14
12
  import type { CompleteRegistrationRequest } from "../schemas/completeRegistrationRequestSchema.ts";
15
13
  import type { RegisterRequest } from "../schemas/registerRequestSchema.ts";
16
14
  import type { RegistrationIntentResponse } from "../schemas/registrationIntentResponseSchema.ts";
15
+ import { UserAudits } from "./UserAudits.ts";
16
+ import { UserNotifications } from "./UserNotifications.ts";
17
17
 
18
18
  /**
19
19
  * Intent stored in cache during the registration flow.
@@ -40,19 +40,34 @@ interface RegistrationIntent {
40
40
  const INTENT_TTL_MINUTES = 10;
41
41
 
42
42
  export class RegistrationService {
43
+ protected readonly alepha = $inject(Alepha);
43
44
  protected readonly log = $logger();
44
45
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
45
46
  protected readonly cryptoProvider = $inject(CryptoProvider);
46
47
  protected readonly verificationController = $client<VerificationController>();
47
- protected readonly userNotifications = $inject(UserNotifications);
48
48
  protected readonly realmProvider = $inject(RealmProvider);
49
- protected readonly auditService = $inject(AuditService);
50
49
 
51
50
  protected readonly intentCache = $cache<RegistrationIntent>({
52
51
  name: "registration-intents",
53
52
  ttl: [INTENT_TTL_MINUTES, "minutes"],
54
53
  });
55
54
 
55
+ protected userAudits(realmName?: string) {
56
+ const realm = this.realmProvider.getRealm(realmName);
57
+ if (realm.features.audits) {
58
+ return this.alepha.inject(UserAudits);
59
+ }
60
+ return undefined;
61
+ }
62
+
63
+ protected userNotifications(realmName?: string) {
64
+ const realm = this.realmProvider.getRealm(realmName);
65
+ if (realm.features.notifications) {
66
+ return this.alepha.inject(UserNotifications);
67
+ }
68
+ return undefined;
69
+ }
70
+
56
71
  /**
57
72
  * Phase 1: Create a registration intent.
58
73
  *
@@ -130,11 +145,11 @@ export class RegistrationService {
130
145
 
131
146
  // Create verification sessions and send codes
132
147
  if (requirements.email && body.email) {
133
- await this.sendEmailVerification(body.email);
148
+ await this.sendEmailVerification(body.email, userRealmName);
134
149
  }
135
150
 
136
151
  if (requirements.phone && body.phoneNumber) {
137
- await this.sendPhoneVerification(body.phoneNumber);
152
+ await this.sendPhoneVerification(body.phoneNumber, userRealmName);
138
153
  }
139
154
 
140
155
  // Generate intent ID and expiration
@@ -291,7 +306,7 @@ export class RegistrationService {
291
306
 
292
307
  const realm = this.realmProvider.getRealm(userRealmName);
293
308
 
294
- await this.auditService.recordUser("create", {
309
+ await this.userAudits(userRealmName)?.recordUser("create", {
295
310
  userId: user.id,
296
311
  userEmail: user.email ?? undefined,
297
312
  userRealm: realm.name,
@@ -353,7 +368,10 @@ export class RegistrationService {
353
368
  /**
354
369
  * Send email verification code.
355
370
  */
356
- protected async sendEmailVerification(email: string): Promise<void> {
371
+ protected async sendEmailVerification(
372
+ email: string,
373
+ realmName?: string,
374
+ ): Promise<void> {
357
375
  this.log.debug("Sending email verification code", { email });
358
376
 
359
377
  const verification =
@@ -362,7 +380,7 @@ export class RegistrationService {
362
380
  body: { target: email },
363
381
  });
364
382
 
365
- await this.userNotifications.emailVerification.push({
383
+ await this.userNotifications(realmName)?.emailVerification.push({
366
384
  contact: email,
367
385
  variables: {
368
386
  email,
@@ -377,7 +395,10 @@ export class RegistrationService {
377
395
  /**
378
396
  * Send phone verification code.
379
397
  */
380
- protected async sendPhoneVerification(phoneNumber: string): Promise<void> {
398
+ protected async sendPhoneVerification(
399
+ phoneNumber: string,
400
+ realmName?: string,
401
+ ): Promise<void> {
381
402
  this.log.debug("Sending phone verification code", { phoneNumber });
382
403
  try {
383
404
  const verification =
@@ -386,7 +407,7 @@ export class RegistrationService {
386
407
  body: { target: phoneNumber },
387
408
  });
388
409
 
389
- await this.userNotifications.phoneVerification.push({
410
+ await this.userNotifications(realmName)?.phoneVerification.push({
390
411
  contact: phoneNumber,
391
412
  variables: {
392
413
  phoneNumber,
@@ -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
+ }