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.
- package/README.md +26 -11
- package/dist/api/audits/index.d.ts +3 -3
- package/dist/api/audits/index.js +3 -3
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +3 -3
- package/dist/api/files/index.js +3 -3
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +47 -4
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +100 -5
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +3 -3
- package/dist/api/keys/index.js +3 -3
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +3 -3
- package/dist/api/notifications/index.js +3 -3
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +263 -263
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +31 -30
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/users/index.d.ts +373 -67
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +273 -72
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +3 -3
- package/dist/api/verifications/index.js +3 -3
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.d.ts +7 -7
- package/dist/batch/index.js +3 -3
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +3 -3
- package/dist/bucket/index.js +6 -6
- package/dist/bucket/index.js.map +1 -1
- package/dist/cache/core/index.d.ts +3 -3
- package/dist/cache/core/index.js +3 -3
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cli/index.d.ts +5607 -20
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +103 -89
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +11 -4
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +8 -6
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +4 -8
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +3 -3
- package/dist/datetime/index.js +3 -3
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +3 -3
- package/dist/email/index.js +8 -8
- package/dist/email/index.js.map +1 -1
- package/dist/fake/index.d.ts +3 -3
- package/dist/fake/index.js +3 -3
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +3 -3
- package/dist/lock/core/index.js +3 -3
- package/dist/lock/core/index.js.map +1 -1
- package/dist/logger/index.d.ts +3 -3
- package/dist/logger/index.js +6 -3
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -3
- package/dist/mcp/index.js +3 -3
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.d.ts +12 -12
- package/dist/orm/index.js +4 -4
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +3 -3
- package/dist/queue/core/index.js +3 -3
- package/dist/queue/core/index.js.map +1 -1
- package/dist/react/auth/index.d.ts +3 -3
- package/dist/react/auth/index.js +3 -3
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.d.ts +3 -3
- package/dist/react/core/index.js +3 -3
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +3 -3
- package/dist/react/form/index.js +3 -3
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.d.ts +3 -3
- package/dist/react/head/index.js +3 -3
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +3 -3
- package/dist/react/i18n/index.js +3 -3
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.css +337 -0
- package/dist/react/intro/index.css.map +1 -0
- package/dist/react/intro/index.d.ts +10 -0
- package/dist/react/intro/index.d.ts.map +1 -0
- package/dist/react/intro/index.js +222 -0
- package/dist/react/intro/index.js.map +1 -0
- package/dist/react/router/index.browser.js +2 -2
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +1 -1
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +5 -5
- package/dist/react/router/index.js.map +1 -1
- package/dist/redis/index.d.ts +17 -17
- package/dist/redis/index.js +3 -3
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts +3 -3
- package/dist/retry/index.js +3 -3
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +3 -3
- package/dist/scheduler/index.js +3 -3
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +3 -3
- package/dist/security/index.js +5 -5
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +3 -3
- package/dist/server/auth/index.js +3 -3
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +3 -3
- package/dist/server/cache/index.js +3 -3
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +3 -3
- package/dist/server/compress/index.js +3 -3
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/cookies/index.d.ts +3 -3
- package/dist/server/cookies/index.js +3 -3
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.d.ts +5 -16
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +13 -29
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +3 -3
- package/dist/server/cors/index.js +3 -3
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/health/index.d.ts +20 -20
- package/dist/server/health/index.js +3 -3
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/helmet/index.d.ts +3 -3
- package/dist/server/helmet/index.js +3 -3
- package/dist/server/helmet/index.js.map +1 -1
- package/dist/server/links/index.d.ts +42 -42
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +3 -3
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +3 -3
- package/dist/server/metrics/index.js +3 -3
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/multipart/index.d.ts +3 -3
- package/dist/server/multipart/index.js +3 -3
- package/dist/server/multipart/index.js.map +1 -1
- package/dist/server/proxy/index.d.ts +3 -3
- package/dist/server/proxy/index.js +3 -3
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +3 -3
- package/dist/server/rate-limit/index.js +3 -3
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.d.ts +3 -3
- package/dist/server/static/index.js +6 -6
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +3 -3
- package/dist/server/swagger/index.js +6 -6
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +3 -3
- package/dist/sms/index.js +6 -6
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.d.ts +3 -3
- package/dist/system/index.js +3 -3
- package/dist/system/index.js.map +1 -1
- package/dist/thread/index.d.ts +3 -3
- package/dist/thread/index.js +3 -3
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/core/index.d.ts +3 -3
- package/dist/topic/core/index.js +3 -3
- package/dist/topic/core/index.js.map +1 -1
- package/dist/vite/index.d.ts +6284 -3
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/websocket/index.d.ts +3 -3
- package/dist/websocket/index.js +3 -3
- package/dist/websocket/index.js.map +1 -1
- package/package.json +7 -2
- package/src/api/audits/index.ts +3 -3
- package/src/api/files/index.ts +3 -3
- package/src/api/jobs/controllers/AdminJobController.ts +15 -2
- package/src/api/jobs/index.ts +4 -3
- package/src/api/jobs/services/JobAudits.spec.ts +89 -0
- package/src/api/jobs/services/JobAudits.ts +101 -0
- package/src/api/keys/index.ts +3 -3
- package/src/api/notifications/index.ts +3 -3
- package/src/api/parameters/index.ts +5 -3
- package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1 -1
- package/src/api/users/__tests__/ApiKeys.spec.ts +1 -1
- package/src/api/users/__tests__/EmailVerification.spec.ts +16 -1
- package/src/api/users/__tests__/PasswordReset.spec.ts +11 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -0
- package/src/api/users/index.ts +8 -9
- package/src/api/users/primitives/$realm.ts +117 -19
- package/src/api/users/providers/RealmProvider.ts +15 -7
- package/src/api/users/services/CredentialService.spec.ts +11 -0
- package/src/api/users/services/CredentialService.ts +47 -24
- package/src/api/users/services/IdentityService.ts +12 -4
- package/src/api/users/services/RegistrationService.spec.ts +11 -0
- package/src/api/users/services/RegistrationService.ts +33 -12
- package/src/api/users/services/SessionService.ts +83 -12
- package/src/api/users/services/UserAudits.ts +47 -0
- package/src/api/users/services/UserFiles.ts +19 -0
- package/src/api/users/services/UserJobs.spec.ts +107 -0
- package/src/api/users/services/UserJobs.ts +62 -0
- package/src/api/users/services/UserParameters.ts +23 -0
- package/src/api/users/services/UserService.ts +34 -17
- package/src/api/verifications/index.ts +3 -3
- package/src/batch/index.ts +3 -3
- package/src/bucket/index.ts +3 -3
- package/src/cache/core/index.ts +3 -3
- package/src/cli/commands/db.ts +9 -0
- package/src/cli/commands/init.spec.ts +2 -17
- package/src/cli/commands/init.ts +37 -1
- package/src/cli/providers/ViteDevServerProvider.ts +5 -2
- package/src/cli/services/AlephaCliUtils.ts +17 -0
- package/src/cli/services/PackageManagerUtils.ts +15 -1
- package/src/cli/services/ProjectScaffolder.ts +8 -13
- package/src/cli/templates/agentMd.ts +2 -25
- package/src/cli/templates/apiAppSecurityTs.ts +37 -2
- package/src/cli/templates/mainCss.ts +2 -32
- package/src/cli/templates/webAppRouterTs.ts +5 -5
- package/src/cli/templates/webHomeComponentTsx.ts +10 -0
- package/src/command/helpers/Runner.ts +14 -1
- package/src/command/index.ts +3 -3
- package/src/core/helpers/primitive.ts +0 -5
- package/src/core/index.ts +3 -3
- package/src/datetime/index.ts +3 -3
- package/src/email/index.ts +3 -3
- package/src/email/providers/LocalEmailProvider.ts +2 -2
- package/src/fake/index.ts +3 -3
- package/src/lock/core/index.ts +3 -3
- package/src/logger/index.ts +3 -3
- package/src/logger/providers/PrettyFormatterProvider.ts +7 -0
- package/src/mcp/index.ts +3 -3
- package/src/orm/index.ts +3 -3
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +1 -1
- package/src/queue/core/index.ts +3 -3
- package/src/react/auth/index.ts +3 -3
- package/src/react/core/index.ts +3 -3
- package/src/react/form/index.ts +3 -3
- package/src/react/head/index.ts +3 -3
- package/src/react/i18n/index.ts +3 -3
- package/src/react/intro/components/GettingStarted.css +334 -0
- package/src/react/intro/components/GettingStarted.tsx +276 -0
- package/src/react/intro/index.ts +1 -0
- package/src/react/router/index.browser.ts +2 -0
- package/src/react/router/index.ts +2 -0
- package/src/redis/index.ts +3 -3
- package/src/retry/index.ts +3 -3
- package/src/router/index.ts +3 -3
- package/src/scheduler/index.ts +3 -3
- package/src/security/index.ts +3 -3
- package/src/security/providers/JwtProvider.ts +2 -2
- package/src/server/auth/index.ts +3 -3
- package/src/server/cache/index.ts +3 -3
- package/src/server/compress/index.ts +3 -3
- package/src/server/cookies/index.ts +3 -3
- package/src/server/core/index.ts +3 -3
- package/src/server/core/primitives/$action.spec.ts +3 -2
- package/src/server/core/primitives/$action.ts +6 -2
- package/src/server/core/providers/NodeHttpServerProvider.ts +2 -15
- package/src/server/core/providers/ServerProvider.ts +4 -2
- package/src/server/core/providers/ServerRouterProvider.ts +5 -27
- package/src/server/cors/index.ts +3 -3
- package/src/server/health/index.ts +3 -3
- package/src/server/helmet/index.ts +3 -3
- package/src/server/links/index.ts +3 -3
- package/src/server/metrics/index.ts +3 -3
- package/src/server/multipart/index.ts +3 -3
- package/src/server/proxy/index.ts +3 -3
- package/src/server/rate-limit/index.ts +3 -3
- package/src/server/static/index.ts +3 -3
- package/src/server/swagger/index.ts +3 -3
- package/src/sms/index.ts +3 -3
- package/src/system/index.ts +3 -3
- package/src/thread/index.ts +3 -3
- package/src/topic/core/index.ts +3 -3
- package/src/websocket/index.ts +3 -3
- package/src/cli/templates/webHelloComponentTsx.ts +0 -30
- /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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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.
|
|
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.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|