alepha 0.20.4 → 0.20.6
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/dist/api/audits/index.d.ts +391 -359
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +23 -1
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +18 -0
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +51 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +33 -14
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +452 -155
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +474 -159
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +32 -4
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +53 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +29 -1
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +55 -13
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/organizations/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +15 -0
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +37 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.d.ts +150 -9
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +237 -28
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +3 -3
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/bin/index.js +0 -0
- package/dist/bucket/index.d.ts +18 -0
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +47 -0
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +24 -0
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +20 -3
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +155 -0
- package/dist/cache/database/index.d.ts.map +1 -0
- package/dist/cache/database/index.js +266 -0
- package/dist/cache/database/index.js.map +1 -0
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +35 -5
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +85 -6
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.js +1 -1
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.browser.js.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/brevo/index.js.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/queue/core/index.js.map +1 -1
- package/dist/queue/core/index.workerd.js.map +1 -1
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.js +2 -0
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/react/websocket/index.js.map +1 -1
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.js.map +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +22 -0
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +12 -0
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.browser.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/etag/index.js.map +1 -1
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/topic/redis/index.js.map +1 -1
- package/dist/websocket/index.browser.js +4 -0
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.js +10 -0
- package/dist/websocket/index.js.map +1 -1
- package/package.json +282 -272
- package/src/api/audits/controllers/AdminAuditController.ts +29 -0
- package/src/api/files/controllers/FileController.ts +24 -0
- package/src/api/files/services/FileService.ts +41 -0
- package/src/api/jobs/__tests__/$job.spec.ts +427 -2
- package/src/api/jobs/entities/jobExecutionEntity.ts +3 -3
- package/src/api/jobs/index.ts +47 -10
- package/src/api/jobs/primitives/$job.ts +22 -9
- package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
- package/src/api/jobs/providers/JobDispatcher.ts +49 -0
- package/src/api/jobs/providers/JobProvider.ts +365 -142
- package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
- package/src/api/jobs/schemas/jobConfigAtom.ts +4 -3
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
- package/src/api/jobs/services/JobService.ts +21 -11
- package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
- package/src/api/keys/services/ApiKeyService.ts +42 -0
- package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
- package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
- package/src/api/notifications/index.ts +13 -3
- package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
- package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
- package/src/api/parameters/services/ParameterProvider.ts +18 -0
- package/src/api/users/__tests__/Registration-emailMode.spec.ts +203 -0
- package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
- package/src/api/users/controllers/AdminSessionController.ts +29 -0
- package/src/api/users/controllers/AdminUserController.ts +32 -0
- package/src/api/users/index.ts +3 -0
- package/src/api/users/services/CredentialService.ts +5 -0
- package/src/api/users/services/RegistrationService.ts +49 -1
- package/src/api/users/services/SessionCrudService.ts +16 -0
- package/src/api/users/services/SessionService.ts +17 -59
- package/src/api/users/services/UsernameSlugger.ts +195 -0
- package/src/bucket/primitives/$bucket.ts +21 -0
- package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
- package/src/bucket/providers/FileStorageProvider.ts +9 -0
- package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
- package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
- package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
- package/src/cache/core/primitives/$cache.ts +20 -3
- package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
- package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
- package/src/cache/database/entities/cacheEntries.ts +55 -0
- package/src/cache/database/index.ts +36 -0
- package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
- package/src/cli/core/services/ProjectScaffolder.ts +0 -2
- package/src/cli/core/tasks/BuildCloudflareTask.ts +17 -3
- package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
- package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
- package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
- package/src/cli/platform/commands/platform.ts +7 -1
- package/src/scheduler/index.ts +14 -0
- package/src/scheduler/providers/CronProvider.ts +13 -0
package/dist/api/users/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { $authApple, $authCredentials, $authFacebook, $authFranceConnect, $authG
|
|
|
12
12
|
import { $etag } from "alepha/server/etag";
|
|
13
13
|
import { randomInt, randomUUID } from "node:crypto";
|
|
14
14
|
import { $cache, CacheProvider } from "alepha/cache";
|
|
15
|
+
import { DatabaseCacheProvider } from "alepha/cache/database";
|
|
15
16
|
import { DateTimeProvider } from "alepha/datetime";
|
|
16
17
|
import { $job } from "alepha/api/jobs";
|
|
17
18
|
import { FileSystemProvider } from "alepha/system";
|
|
@@ -155,6 +156,12 @@ const fieldRequirement = (description) => t.union([
|
|
|
155
156
|
t.const("optional"),
|
|
156
157
|
t.const("required")
|
|
157
158
|
], { description });
|
|
159
|
+
const usernameFieldRequirement = (description) => t.union([
|
|
160
|
+
t.const("none"),
|
|
161
|
+
t.const("optional"),
|
|
162
|
+
t.const("required"),
|
|
163
|
+
t.const("email")
|
|
164
|
+
], { description });
|
|
158
165
|
const realmAuthSettingsAtom = $atom({
|
|
159
166
|
name: "alepha.api.users.realmAuthSettings",
|
|
160
167
|
schema: t.object({
|
|
@@ -163,8 +170,9 @@ const realmAuthSettingsAtom = $atom({
|
|
|
163
170
|
logoUrl: t.optional(t.string({ description: "Logo URL for auth pages" })),
|
|
164
171
|
registrationAllowed: t.boolean({ description: "Enable user self-registration" }),
|
|
165
172
|
email: fieldRequirement("Email address field requirement for user accounts"),
|
|
166
|
-
username:
|
|
173
|
+
username: usernameFieldRequirement("Username field requirement for user accounts"),
|
|
167
174
|
usernameRegExp: t.string({ description: "Regular expression that usernames must match (if username is enabled)" }),
|
|
175
|
+
usernameBlocklist: t.array(t.text(), { description: "Usernames that the slugger / manual registration must reject. Default empty so apps can register `admin`/`root`/`me`/etc. if they want; populate it explicitly for handles you want to keep off-limits." }),
|
|
168
176
|
phoneNumber: fieldRequirement("Phone number field requirement for user accounts"),
|
|
169
177
|
verifyEmailRequired: t.boolean({ description: "Require email verification for user accounts" }),
|
|
170
178
|
verifyPhoneRequired: t.boolean({ description: "Require phone verification for user accounts" }),
|
|
@@ -212,7 +220,8 @@ const realmAuthSettingsAtom = $atom({
|
|
|
212
220
|
registrationAllowed: true,
|
|
213
221
|
email: "required",
|
|
214
222
|
username: "none",
|
|
215
|
-
usernameRegExp: "^[a-zA-Z0-9_]{3,30}$",
|
|
223
|
+
usernameRegExp: "^[a-zA-Z0-9_-]{3,30}$",
|
|
224
|
+
usernameBlocklist: [],
|
|
216
225
|
phoneNumber: "none",
|
|
217
226
|
verifyEmailRequired: false,
|
|
218
227
|
verifyPhoneRequired: false,
|
|
@@ -562,6 +571,19 @@ var SessionCrudService = class {
|
|
|
562
571
|
await this.sessions(userRealmName).deleteById(id);
|
|
563
572
|
this.log.info("Session deleted", { id });
|
|
564
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Delete many sessions by ID in one repository call.
|
|
576
|
+
*/
|
|
577
|
+
async deleteSessions(ids, userRealmName) {
|
|
578
|
+
if (ids.length === 0) return [];
|
|
579
|
+
this.log.trace("Deleting sessions", {
|
|
580
|
+
count: ids.length,
|
|
581
|
+
userRealmName
|
|
582
|
+
});
|
|
583
|
+
const deleted = await this.sessions(userRealmName).deleteMany({ id: { inArray: ids } });
|
|
584
|
+
this.log.info("Sessions deleted", { count: deleted.length });
|
|
585
|
+
return deleted.map(String);
|
|
586
|
+
}
|
|
565
587
|
};
|
|
566
588
|
//#endregion
|
|
567
589
|
//#region ../../src/api/users/controllers/AdminSessionController.ts
|
|
@@ -623,6 +645,27 @@ var AdminSessionController = class {
|
|
|
623
645
|
};
|
|
624
646
|
}
|
|
625
647
|
});
|
|
648
|
+
/**
|
|
649
|
+
* Delete many sessions in one repository call.
|
|
650
|
+
*/
|
|
651
|
+
deleteSessions = $action({
|
|
652
|
+
method: "POST",
|
|
653
|
+
path: `${this.url}/delete`,
|
|
654
|
+
group: this.group,
|
|
655
|
+
use: [$secure({ permissions: ["admin:session:delete"] })],
|
|
656
|
+
description: "Delete many sessions",
|
|
657
|
+
schema: {
|
|
658
|
+
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
659
|
+
body: t.object({ ids: t.array(t.uuid(), {
|
|
660
|
+
minItems: 1,
|
|
661
|
+
maxItems: 1e3
|
|
662
|
+
}) }),
|
|
663
|
+
response: t.object({ deleted: t.array(t.string()) })
|
|
664
|
+
},
|
|
665
|
+
handler: async ({ body, query }) => {
|
|
666
|
+
return { deleted: await this.sessionService.deleteSessions(body.ids, query.userRealmName) };
|
|
667
|
+
}
|
|
668
|
+
});
|
|
626
669
|
};
|
|
627
670
|
//#endregion
|
|
628
671
|
//#region ../../src/api/users/schemas/createUserSchema.ts
|
|
@@ -1176,6 +1219,34 @@ var AdminUserController = class {
|
|
|
1176
1219
|
};
|
|
1177
1220
|
}
|
|
1178
1221
|
});
|
|
1222
|
+
/**
|
|
1223
|
+
* Delete many users in one request. Each id is processed sequentially so
|
|
1224
|
+
* cascades and side-effects run as if called one-by-one. Errors on a single
|
|
1225
|
+
* id surface with that id in the response.
|
|
1226
|
+
*/
|
|
1227
|
+
deleteUsers = $action({
|
|
1228
|
+
method: "POST",
|
|
1229
|
+
path: `${this.url}/delete`,
|
|
1230
|
+
group: this.group,
|
|
1231
|
+
use: [$secure({ permissions: ["admin:user:delete"] })],
|
|
1232
|
+
description: "Delete many users",
|
|
1233
|
+
schema: {
|
|
1234
|
+
query: t.object({ userRealmName: t.optional(t.string()) }),
|
|
1235
|
+
body: t.object({ ids: t.array(t.uuid(), {
|
|
1236
|
+
minItems: 1,
|
|
1237
|
+
maxItems: 1e3
|
|
1238
|
+
}) }),
|
|
1239
|
+
response: t.object({ deleted: t.array(t.uuid()) })
|
|
1240
|
+
},
|
|
1241
|
+
handler: async ({ body, query }) => {
|
|
1242
|
+
const deleted = [];
|
|
1243
|
+
for (const id of body.ids) {
|
|
1244
|
+
await this.userService.deleteUser(id, query.userRealmName);
|
|
1245
|
+
deleted.push(id);
|
|
1246
|
+
}
|
|
1247
|
+
return { deleted };
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1179
1250
|
};
|
|
1180
1251
|
//#endregion
|
|
1181
1252
|
//#region ../../src/api/users/schemas/realmConfigSchema.ts
|
|
@@ -1331,6 +1402,7 @@ var CredentialService = class {
|
|
|
1331
1402
|
if (this.realmProvider.getRealm(realmName).features.notifications) return this.alepha.inject(UserNotifications);
|
|
1332
1403
|
}
|
|
1333
1404
|
intentCache = $cache({
|
|
1405
|
+
provider: DatabaseCacheProvider,
|
|
1334
1406
|
name: "api:users:password-reset-intents",
|
|
1335
1407
|
ttl: [INTENT_TTL_MINUTES$1, "minutes"]
|
|
1336
1408
|
});
|
|
@@ -1558,6 +1630,139 @@ var CredentialService = class {
|
|
|
1558
1630
|
}
|
|
1559
1631
|
};
|
|
1560
1632
|
//#endregion
|
|
1633
|
+
//#region ../../src/api/users/services/UsernameSlugger.ts
|
|
1634
|
+
/**
|
|
1635
|
+
* Derive stable, URL-safe usernames from email addresses.
|
|
1636
|
+
*
|
|
1637
|
+
* Used by the registration flow when `realm.settings.username === "email"`,
|
|
1638
|
+
* and reusable from any custom user-creation site (e.g. an OAuth-only flow
|
|
1639
|
+
* that wants the same handle convention).
|
|
1640
|
+
*
|
|
1641
|
+
* **Slug rule** — the local-part of the email is kept as-is (gmail
|
|
1642
|
+
* `+suffix` retained for predictability). Everything outside `[a-z0-9]` is
|
|
1643
|
+
* replaced with `-`, runs of `-` are collapsed, leading/trailing `-` are
|
|
1644
|
+
* trimmed, lowercased. If the result is shorter than {@link MIN_LENGTH} it
|
|
1645
|
+
* is padded with random alphanumerics. Result is clamped to
|
|
1646
|
+
* {@link MAX_LENGTH}.
|
|
1647
|
+
*
|
|
1648
|
+
* **Collision retry** — `pickAvailable()` checks the realm's `users` table
|
|
1649
|
+
* and the realm's `usernameBlocklist`. On hit, it appends `-<4 random>` and
|
|
1650
|
+
* retries up to {@link MAX_RETRIES} times. Best-effort against concurrent
|
|
1651
|
+
* registrations: the unique index on `(realm, lower(username))` is the
|
|
1652
|
+
* authoritative race guard, callers that hit a unique-violation should
|
|
1653
|
+
* call `pickAvailable` again with a fresh suffix.
|
|
1654
|
+
*
|
|
1655
|
+
* @see RegistrationService.createRegistrationIntent
|
|
1656
|
+
*/
|
|
1657
|
+
var UsernameSlugger = class UsernameSlugger {
|
|
1658
|
+
realmProvider = $inject(RealmProvider);
|
|
1659
|
+
log = $logger();
|
|
1660
|
+
/**
|
|
1661
|
+
* Floor for derived usernames. Shorter slugs are padded with random
|
|
1662
|
+
* alphanumerics. Matches the lower bound of the default `usernameRegExp`.
|
|
1663
|
+
*/
|
|
1664
|
+
static MIN_LENGTH = 3;
|
|
1665
|
+
/**
|
|
1666
|
+
* Ceiling for derived usernames. Matches the upper bound of the default
|
|
1667
|
+
* `usernameRegExp` and gives enough headroom for the random suffix added
|
|
1668
|
+
* on collisions.
|
|
1669
|
+
*/
|
|
1670
|
+
static MAX_LENGTH = 30;
|
|
1671
|
+
/**
|
|
1672
|
+
* Length of the random suffix appended on collision (e.g. `-3dp6`).
|
|
1673
|
+
* 36⁴ ≈ 1.6M variants per base — plenty for a tiny number of retries.
|
|
1674
|
+
*/
|
|
1675
|
+
static SUFFIX_LENGTH = 4;
|
|
1676
|
+
/**
|
|
1677
|
+
* How many times `pickAvailable` retries before giving up.
|
|
1678
|
+
*/
|
|
1679
|
+
static MAX_RETRIES = 5;
|
|
1680
|
+
/**
|
|
1681
|
+
* Alphabet used for the random suffix and for padding short slugs.
|
|
1682
|
+
*/
|
|
1683
|
+
static ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1684
|
+
/**
|
|
1685
|
+
* Default replacement when the email's local-part contains no
|
|
1686
|
+
* `[a-z0-9]` characters at all (rare but possible: `é@example.com`).
|
|
1687
|
+
*/
|
|
1688
|
+
static EMPTY_LOCAL_FALLBACK = "user";
|
|
1689
|
+
/**
|
|
1690
|
+
* Sanitize an email into a base username slug.
|
|
1691
|
+
*
|
|
1692
|
+
* Pure function — no DB access. Always returns a string that satisfies
|
|
1693
|
+
* `^[a-z0-9-]{MIN_LENGTH,MAX_LENGTH}$`.
|
|
1694
|
+
*
|
|
1695
|
+
* @example
|
|
1696
|
+
* slug("ni.foures+testkv@gmail.com") // "ni-foures-testkv"
|
|
1697
|
+
* slug("john.doe@example.com") // "john-doe"
|
|
1698
|
+
* slug("é@example.com") // "user-XXX" (padded)
|
|
1699
|
+
*/
|
|
1700
|
+
slug(email) {
|
|
1701
|
+
const raw = (email ?? "").trim();
|
|
1702
|
+
const at = raw.indexOf("@");
|
|
1703
|
+
let result = (at > 0 ? raw.slice(0, at) : raw).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || UsernameSlugger.EMPTY_LOCAL_FALLBACK;
|
|
1704
|
+
if (result.length < UsernameSlugger.MIN_LENGTH) {
|
|
1705
|
+
const needed = UsernameSlugger.MIN_LENGTH - result.length;
|
|
1706
|
+
result += this.randomSuffix(needed);
|
|
1707
|
+
}
|
|
1708
|
+
return this.clamp(result);
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Find an available username for the realm, starting from `base`.
|
|
1712
|
+
*
|
|
1713
|
+
* Returns `base` when nothing collides. On a hit (existing row OR
|
|
1714
|
+
* blocklisted name) appends `-<4 random>` and tries again, up to
|
|
1715
|
+
* {@link MAX_RETRIES} times.
|
|
1716
|
+
*
|
|
1717
|
+
* The check is best-effort: a concurrent registration may still claim
|
|
1718
|
+
* the same value before the caller's INSERT runs, in which case the DB
|
|
1719
|
+
* unique index throws and the caller should retry.
|
|
1720
|
+
*/
|
|
1721
|
+
async pickAvailable(realmName, base) {
|
|
1722
|
+
const blocklist = await this.getBlocklist(realmName);
|
|
1723
|
+
const repo = this.realmProvider.userRepository(realmName);
|
|
1724
|
+
const realm = this.realmProvider.getRealm(realmName);
|
|
1725
|
+
const isAvailable = async (candidate) => {
|
|
1726
|
+
if (this.isBlockedAgainst(candidate, blocklist)) return false;
|
|
1727
|
+
return !await repo.findOne({ where: {
|
|
1728
|
+
realm: { eq: realm.name },
|
|
1729
|
+
username: { ilike: candidate }
|
|
1730
|
+
} });
|
|
1731
|
+
};
|
|
1732
|
+
if (await isAvailable(base)) return base;
|
|
1733
|
+
const reserve = 1 + UsernameSlugger.SUFFIX_LENGTH;
|
|
1734
|
+
const trimmedBase = base.length > UsernameSlugger.MAX_LENGTH - reserve ? base.slice(0, UsernameSlugger.MAX_LENGTH - reserve) : base;
|
|
1735
|
+
for (let i = 0; i < UsernameSlugger.MAX_RETRIES; i++) {
|
|
1736
|
+
const candidate = `${trimmedBase}-${this.randomSuffix(UsernameSlugger.SUFFIX_LENGTH)}`;
|
|
1737
|
+
if (await isAvailable(candidate)) return candidate;
|
|
1738
|
+
}
|
|
1739
|
+
throw new AlephaError(`Could not find an available username starting from "${base}" after ${UsernameSlugger.MAX_RETRIES} attempts.`);
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Check a name against the realm's `usernameBlocklist`. Case-insensitive.
|
|
1743
|
+
*/
|
|
1744
|
+
async isBlocked(realmName, name) {
|
|
1745
|
+
const blocklist = await this.getBlocklist(realmName);
|
|
1746
|
+
return this.isBlockedAgainst(name, blocklist);
|
|
1747
|
+
}
|
|
1748
|
+
async getBlocklist(realmName) {
|
|
1749
|
+
const list = (await this.realmProvider.getRealm(realmName).getSettings())?.usernameBlocklist ?? [];
|
|
1750
|
+
return new Set(list.map((b) => b.toLowerCase()));
|
|
1751
|
+
}
|
|
1752
|
+
isBlockedAgainst(name, blocklist) {
|
|
1753
|
+
return blocklist.has(name.toLowerCase());
|
|
1754
|
+
}
|
|
1755
|
+
clamp(s) {
|
|
1756
|
+
return s.length > UsernameSlugger.MAX_LENGTH ? s.slice(0, UsernameSlugger.MAX_LENGTH) : s;
|
|
1757
|
+
}
|
|
1758
|
+
randomSuffix(length) {
|
|
1759
|
+
const chars = UsernameSlugger.ALPHABET;
|
|
1760
|
+
let out = "";
|
|
1761
|
+
for (let i = 0; i < length; i++) out += chars[Math.floor(Math.random() * chars.length)];
|
|
1762
|
+
return out;
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
//#endregion
|
|
1561
1766
|
//#region ../../src/api/users/services/RegistrationService.ts
|
|
1562
1767
|
const INTENT_TTL_MINUTES = 10;
|
|
1563
1768
|
var RegistrationService = class {
|
|
@@ -1569,11 +1774,14 @@ var RegistrationService = class {
|
|
|
1569
1774
|
realmProvider = $inject(RealmProvider);
|
|
1570
1775
|
credentialService = $inject(CredentialService);
|
|
1571
1776
|
captchaProvider = $inject(CaptchaProvider);
|
|
1777
|
+
usernameSlugger = $inject(UsernameSlugger);
|
|
1572
1778
|
intentCache = $cache({
|
|
1779
|
+
provider: DatabaseCacheProvider,
|
|
1573
1780
|
name: "api:users:registrations",
|
|
1574
1781
|
ttl: [INTENT_TTL_MINUTES, "minutes"]
|
|
1575
1782
|
});
|
|
1576
1783
|
rateLimitCache = $cache({
|
|
1784
|
+
provider: DatabaseCacheProvider,
|
|
1577
1785
|
name: "api:users:registration-rate-limit",
|
|
1578
1786
|
ttl: [15, "minutes"]
|
|
1579
1787
|
});
|
|
@@ -1615,6 +1823,7 @@ var RegistrationService = class {
|
|
|
1615
1823
|
this.log.debug("Registration rejected: username required", { userRealmName });
|
|
1616
1824
|
throw new BadRequestError("Username is required");
|
|
1617
1825
|
}
|
|
1826
|
+
if (realmSettings?.username === "email") body.username = void 0;
|
|
1618
1827
|
if (body.username) {
|
|
1619
1828
|
const usernameRegExp = realmSettings?.usernameRegExp;
|
|
1620
1829
|
if (usernameRegExp) {
|
|
@@ -1626,6 +1835,13 @@ var RegistrationService = class {
|
|
|
1626
1835
|
throw new BadRequestError("Username does not meet the required format");
|
|
1627
1836
|
}
|
|
1628
1837
|
}
|
|
1838
|
+
if (await this.usernameSlugger.isBlocked(userRealmName, body.username)) {
|
|
1839
|
+
this.log.debug("Registration rejected: username is blocked", {
|
|
1840
|
+
userRealmName,
|
|
1841
|
+
username: body.username
|
|
1842
|
+
});
|
|
1843
|
+
throw new BadRequestError("This username is not available");
|
|
1844
|
+
}
|
|
1629
1845
|
}
|
|
1630
1846
|
if (realmSettings?.email === "required" && !body.email) {
|
|
1631
1847
|
this.log.debug("Registration rejected: email required", { userRealmName });
|
|
@@ -1635,6 +1851,11 @@ var RegistrationService = class {
|
|
|
1635
1851
|
this.log.debug("Registration rejected: phone required", { userRealmName });
|
|
1636
1852
|
throw new BadRequestError("Phone number is required");
|
|
1637
1853
|
}
|
|
1854
|
+
if (realmSettings?.username === "email") {
|
|
1855
|
+
if (!body.email) throw new BadRequestError("Email is required to derive a username from email");
|
|
1856
|
+
const base = this.usernameSlugger.slug(body.email);
|
|
1857
|
+
body.username = await this.usernameSlugger.pickAvailable(userRealmName, base);
|
|
1858
|
+
}
|
|
1638
1859
|
await this.checkUserAvailability(body, userRealmName);
|
|
1639
1860
|
this.credentialService.validatePasswordPolicy(body.password, realmSettings.passwordPolicy);
|
|
1640
1861
|
if (realmSettings?.captchaRequired === true) {
|
|
@@ -2166,6 +2387,7 @@ var SessionService = class SessionService {
|
|
|
2166
2387
|
realmProvider = $inject(RealmProvider);
|
|
2167
2388
|
fileController = $client();
|
|
2168
2389
|
cacheProvider = $inject(CacheProvider);
|
|
2390
|
+
usernameSlugger = $inject(UsernameSlugger);
|
|
2169
2391
|
userAudits(realmName) {
|
|
2170
2392
|
if (this.realmProvider.getRealm(realmName).features.audits) return this.alepha.inject(UserAudits);
|
|
2171
2393
|
}
|
|
@@ -2220,30 +2442,16 @@ var SessionService = class SessionService {
|
|
|
2220
2442
|
/**
|
|
2221
2443
|
* Generate a unique username from an OAuth profile.
|
|
2222
2444
|
*
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
*
|
|
2226
|
-
*
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
const
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
if (realmSettings?.usernameRegExp) try {
|
|
2234
|
-
if (!new RegExp(realmSettings.usernameRegExp).test(candidate)) candidate = candidate.replace(/[^a-zA-Z0-9_]/g, "");
|
|
2235
|
-
} catch {}
|
|
2236
|
-
if (candidate.length < 3) candidate = `user${candidate}`;
|
|
2237
|
-
candidate = candidate.slice(0, maxLength - 2);
|
|
2238
|
-
const isAvailable = async (name) => {
|
|
2239
|
-
return !await users.findOne({ where: { username: { ilike: name } } });
|
|
2240
|
-
};
|
|
2241
|
-
if (await isAvailable(candidate)) return candidate;
|
|
2242
|
-
for (let i = 2; i <= maxSuffixAttempts + 1; i++) {
|
|
2243
|
-
const withSuffix = `${candidate}${i}`;
|
|
2244
|
-
if (withSuffix.length <= maxLength && await isAvailable(withSuffix)) return withSuffix;
|
|
2245
|
-
}
|
|
2246
|
-
return `user${Math.random().toString(36).slice(2, 8)}`;
|
|
2445
|
+
* Routes through {@link UsernameSlugger}, which is the same code path as
|
|
2446
|
+
* `username: "email"` registration. The OAuth profile's email is the
|
|
2447
|
+
* primary signal; if absent (rare — most IDPs return one), we fall back
|
|
2448
|
+
* to `profile.name`, then to a random handle. The slugger applies the
|
|
2449
|
+
* realm's `usernameBlocklist` and retries on collision.
|
|
2450
|
+
*/
|
|
2451
|
+
async generateUniqueUsername(profile, _realmSettings, _users, realmName) {
|
|
2452
|
+
const seed = profile.email ?? profile.name ?? `user-${Math.random().toString(36).slice(2, 8)}`;
|
|
2453
|
+
const base = this.usernameSlugger.slug(seed);
|
|
2454
|
+
return this.usernameSlugger.pickAvailable(realmName, base);
|
|
2247
2455
|
}
|
|
2248
2456
|
/**
|
|
2249
2457
|
* Random delay to prevent timing attacks (50-200ms)
|
|
@@ -2654,7 +2862,7 @@ var SessionService = class SessionService {
|
|
|
2654
2862
|
});
|
|
2655
2863
|
throw new BadRequestError("Account doesn't exist");
|
|
2656
2864
|
}
|
|
2657
|
-
const username = await this.generateUniqueUsername(profile, realmSettings, users);
|
|
2865
|
+
const username = await this.generateUniqueUsername(profile, realmSettings, users, userRealmName);
|
|
2658
2866
|
const user = await users.create({
|
|
2659
2867
|
realm: realm.name,
|
|
2660
2868
|
username,
|
|
@@ -2921,6 +3129,7 @@ const AlephaApiUsers = $module({
|
|
|
2921
3129
|
CredentialService,
|
|
2922
3130
|
RegistrationService,
|
|
2923
3131
|
UserService,
|
|
3132
|
+
UsernameSlugger,
|
|
2924
3133
|
IdentityService,
|
|
2925
3134
|
UserController,
|
|
2926
3135
|
AdminUserController,
|
|
@@ -2936,6 +3145,6 @@ const AlephaApiUsers = $module({
|
|
|
2936
3145
|
]
|
|
2937
3146
|
});
|
|
2938
3147
|
//#endregion
|
|
2939
|
-
export { $realm, AdminIdentityController, AdminSessionController, AdminUserController, AlephaApiUsers, CredentialService, DEFAULT_USER_REALM_NAME, IdentityService, RealmController, RealmProvider, RegistrationService, SessionCrudService, SessionService, UserAudits, UserBuckets, UserController, UserJobs, UserNotifications, UserService, completePasswordResetRequestSchema, completeRegistrationRequestSchema, createUserSchema, identities, identityQuerySchema, identityResourceSchema, loginSchema, passwordResetIntentResponseSchema, realmAuthSettingsAtom, realmConfigSchema, registerSchema, registrationIntentResponseSchema, resetPasswordRequestSchema, resetPasswordSchema, sessionQuerySchema, sessionResourceSchema, sessions, updateUserSchema, userQuerySchema, userResourceSchema, users };
|
|
3148
|
+
export { $realm, AdminIdentityController, AdminSessionController, AdminUserController, AlephaApiUsers, CredentialService, DEFAULT_USER_REALM_NAME, IdentityService, RealmController, RealmProvider, RegistrationService, SessionCrudService, SessionService, UserAudits, UserBuckets, UserController, UserJobs, UserNotifications, UserService, UsernameSlugger, completePasswordResetRequestSchema, completeRegistrationRequestSchema, createUserSchema, identities, identityQuerySchema, identityResourceSchema, loginSchema, passwordResetIntentResponseSchema, realmAuthSettingsAtom, realmConfigSchema, registerSchema, registrationIntentResponseSchema, resetPasswordRequestSchema, resetPasswordSchema, sessionQuerySchema, sessionResourceSchema, sessions, updateUserSchema, userQuerySchema, userResourceSchema, users };
|
|
2940
3149
|
|
|
2941
3150
|
//# sourceMappingURL=index.js.map
|