alepha 0.19.3 → 0.19.4
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/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/dist/api/audits/index.d.ts +8 -8
- package/dist/api/invitations/index.d.ts +790 -0
- package/dist/api/invitations/index.d.ts.map +1 -0
- package/dist/api/invitations/index.js +665 -0
- package/dist/api/invitations/index.js.map +1 -0
- package/dist/api/jobs/index.browser.js +8 -9
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +99 -43
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +257 -40
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +5 -5
- package/dist/api/notifications/index.browser.js +0 -1
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +3 -3
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +0 -1
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.browser.js +112 -1
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +90 -3
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +79 -12
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/{billing → api/payments}/index.d.ts +67 -49
- package/dist/api/payments/index.d.ts.map +1 -0
- package/dist/{billing → api/payments}/index.js +108 -74
- package/dist/api/payments/index.js.map +1 -0
- package/dist/api/subscriptions/index.d.ts +1692 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1870 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +18 -2
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +167 -34
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +13 -13
- package/dist/api/workflows/index.browser.js +246 -0
- package/dist/api/workflows/index.browser.js.map +1 -0
- package/dist/api/workflows/index.d.ts +1618 -0
- package/dist/api/workflows/index.d.ts.map +1 -0
- package/dist/api/workflows/index.js +1504 -0
- package/dist/api/workflows/index.js.map +1 -0
- package/dist/cli/core/index.d.ts +44 -28
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +16 -61
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +31 -8
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +79 -24
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/core/index.browser.js +21 -2
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +33 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +21 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +21 -2
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +21 -2
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.js +24 -8
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +0 -18
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +0 -17
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +1 -13
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +0 -17
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +3 -3
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +3 -3
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/router/index.browser.js +25 -3
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +16 -1
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +25 -3
- package/dist/react/router/index.js.map +1 -1
- package/dist/security/index.d.ts +28 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +28 -0
- package/dist/security/index.js.map +1 -1
- package/package.json +37 -20
- package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
- package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
- package/src/api/invitations/controllers/InvitationController.ts +84 -0
- package/src/api/invitations/entities/invitations.ts +33 -0
- package/src/api/invitations/index.ts +65 -0
- package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
- package/src/api/invitations/providers/InvitationProvider.ts +45 -0
- package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
- package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
- package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
- package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
- package/src/api/invitations/services/InvitationService.ts +556 -0
- package/src/api/jobs/__tests__/$job.spec.ts +876 -0
- package/src/api/jobs/controllers/AdminJobController.ts +44 -0
- package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
- package/src/api/jobs/index.ts +0 -3
- package/src/api/jobs/primitives/$job.ts +22 -11
- package/src/api/jobs/providers/JobProvider.ts +229 -19
- package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
- package/src/api/jobs/services/JobService.ts +51 -12
- package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
- package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
- package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
- package/src/api/parameters/index.browser.ts +12 -0
- package/src/api/parameters/primitives/$parameter.ts +20 -3
- package/src/api/parameters/services/ParameterProvider.ts +48 -7
- package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
- package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
- package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
- package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
- package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
- package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
- package/src/{billing → api/payments}/index.ts +31 -25
- package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
- package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
- package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
- package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +144 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
- package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
- package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
- package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
- package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
- package/src/api/users/__tests__/SessionService.spec.ts +142 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
- package/src/api/users/controllers/UserController.ts +3 -8
- package/src/api/users/notifications/UserNotifications.ts +23 -0
- package/src/api/users/schemas/loginSchema.ts +1 -1
- package/src/api/users/services/CredentialService.ts +51 -4
- package/src/api/users/services/RegistrationService.ts +38 -9
- package/src/api/users/services/SessionService.ts +62 -9
- package/src/api/users/services/UserService.ts +21 -12
- package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
- package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
- package/src/api/workflows/entities/workflowExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
- package/src/api/workflows/index.browser.ts +22 -0
- package/src/api/workflows/index.ts +124 -0
- package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
- package/src/api/workflows/primitives/$workflow.ts +202 -0
- package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
- package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
- package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
- package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
- package/src/api/workflows/services/WorkflowService.ts +382 -0
- package/src/cli/core/templates/webAppRouterTs.ts +5 -58
- package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
- package/src/cli/vendor/services/VendorService.ts +126 -27
- package/src/core/__tests__/TypeProvider.spec.ts +4 -2
- package/src/core/providers/SchemaValidator.ts +1 -1
- package/src/core/providers/TypeProvider.ts +46 -3
- package/src/orm/__tests__/enums.spec.ts +22 -29
- package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
- package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
- package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
- package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
- package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
- package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
- package/src/security/primitives/$secure.ts +28 -0
- package/dist/billing/index.d.ts.map +0 -1
- package/dist/billing/index.js.map +0 -1
- package/src/billing/__tests__/BillingService.spec.ts +0 -136
- /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
- /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
- /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
package/dist/api/users/index.js
CHANGED
|
@@ -168,6 +168,8 @@ const realmAuthSettingsAtom = $atom({
|
|
|
168
168
|
resetPasswordAllowed: t.boolean({ description: "Enable forgot password functionality" }),
|
|
169
169
|
adminEmails: t.array(t.email(), { description: "List of email addresses that are automatically promoted to admin role on login" }),
|
|
170
170
|
adminUsernames: t.array(t.text(), { description: "List of usernames that are automatically promoted to admin role on login" }),
|
|
171
|
+
defaultRoles: t.array(t.string(), { description: "Default roles assigned to newly registered users" }),
|
|
172
|
+
verifyEmailUrl: t.optional(t.string({ description: "Base URL for email verification links (used when verification method is 'link'). Token and email are appended as query params." })),
|
|
171
173
|
passwordPolicy: t.object({
|
|
172
174
|
minLength: t.integer({
|
|
173
175
|
description: "Minimum password length",
|
|
@@ -209,6 +211,7 @@ const realmAuthSettingsAtom = $atom({
|
|
|
209
211
|
firstNameLastName: "none",
|
|
210
212
|
adminEmails: [],
|
|
211
213
|
adminUsernames: [],
|
|
214
|
+
defaultRoles: ["user"],
|
|
212
215
|
passwordPolicy: {
|
|
213
216
|
minLength: 8,
|
|
214
217
|
requireUppercase: true,
|
|
@@ -723,6 +726,27 @@ var UserNotifications = class {
|
|
|
723
726
|
expiresInMinutes: t.number()
|
|
724
727
|
})
|
|
725
728
|
});
|
|
729
|
+
accountLockout = $notification({
|
|
730
|
+
category: "security",
|
|
731
|
+
description: "Email sent to users when their account is temporarily locked due to too many failed login attempts.",
|
|
732
|
+
critical: true,
|
|
733
|
+
sensitive: true,
|
|
734
|
+
email: {
|
|
735
|
+
subject: "Account temporarily locked",
|
|
736
|
+
body: (it) => `
|
|
737
|
+
<h1>Account Temporarily Locked</h1>
|
|
738
|
+
<p>Hi ${it.email},</p>
|
|
739
|
+
<p>Your account has been temporarily locked due to too many failed login attempts.</p>
|
|
740
|
+
<p>If this was you, please wait ${it.lockoutMinutes} minutes before trying again. If you've forgotten your password, you can reset it using the password reset feature.</p>
|
|
741
|
+
<p>If this wasn't you, someone may be trying to access your account. We recommend changing your password as soon as possible.</p>
|
|
742
|
+
<p>Best regards,<br>The Team</p>
|
|
743
|
+
`
|
|
744
|
+
},
|
|
745
|
+
schema: t.object({
|
|
746
|
+
email: t.string({ format: "email" }),
|
|
747
|
+
lockoutMinutes: t.number()
|
|
748
|
+
})
|
|
749
|
+
});
|
|
726
750
|
emailVerificationLink = $notification({
|
|
727
751
|
category: "security",
|
|
728
752
|
description: "Email sent to users with a link to verify their email address.",
|
|
@@ -776,7 +800,7 @@ var UserService = class {
|
|
|
776
800
|
* @param method - The verification method: "code" (default) or "link".
|
|
777
801
|
* @param verifyUrl - Base URL for verification link (required when method is "link").
|
|
778
802
|
*/
|
|
779
|
-
async requestEmailVerification(email, userRealmName, method = "code"
|
|
803
|
+
async requestEmailVerification(email, userRealmName, method = "code") {
|
|
780
804
|
this.log.trace("Requesting email verification", {
|
|
781
805
|
email,
|
|
782
806
|
userRealmName,
|
|
@@ -800,10 +824,12 @@ var UserService = class {
|
|
|
800
824
|
body: { target: email }
|
|
801
825
|
});
|
|
802
826
|
if (method === "link") {
|
|
803
|
-
const
|
|
827
|
+
const realmSettings = await this.realmProvider.getRealm(userRealmName).getSettings();
|
|
828
|
+
const baseUrl = realmSettings.verifyEmailUrl ?? "/verify-email";
|
|
829
|
+
const url = new URL(baseUrl, "http://localhost");
|
|
804
830
|
url.searchParams.set("email", email);
|
|
805
831
|
url.searchParams.set("token", verification.token);
|
|
806
|
-
const fullVerifyUrl =
|
|
832
|
+
const fullVerifyUrl = realmSettings.verifyEmailUrl ? `${baseUrl}${url.search}` : url.pathname + url.search;
|
|
807
833
|
await this.userNotifications(userRealmName)?.emailVerificationLink.push({
|
|
808
834
|
contact: email,
|
|
809
835
|
variables: {
|
|
@@ -936,27 +962,37 @@ var UserService = class {
|
|
|
936
962
|
userRealmName
|
|
937
963
|
});
|
|
938
964
|
const realm = this.realmProvider.getRealm(userRealmName);
|
|
965
|
+
const realmSettings = await realm.getSettings();
|
|
939
966
|
if (data.username) {
|
|
940
|
-
if (await this.users(userRealmName).findOne({ where: {
|
|
967
|
+
if (await this.users(userRealmName).findOne({ where: {
|
|
968
|
+
realm: realm.name,
|
|
969
|
+
username: { ilike: data.username }
|
|
970
|
+
} })) {
|
|
941
971
|
this.log.debug("Username already taken", { username: data.username });
|
|
942
972
|
throw new BadRequestError("User with this username already exists");
|
|
943
973
|
}
|
|
944
974
|
}
|
|
945
975
|
if (data.email) {
|
|
946
|
-
if (await this.users(userRealmName).findOne({ where: {
|
|
976
|
+
if (await this.users(userRealmName).findOne({ where: {
|
|
977
|
+
realm: realm.name,
|
|
978
|
+
email: { eq: data.email }
|
|
979
|
+
} })) {
|
|
947
980
|
this.log.debug("Email already taken", { email: data.email });
|
|
948
981
|
throw new BadRequestError("User with this email already exists");
|
|
949
982
|
}
|
|
950
983
|
}
|
|
951
984
|
if (data.phoneNumber) {
|
|
952
|
-
if (await this.users(userRealmName).findOne({ where: {
|
|
985
|
+
if (await this.users(userRealmName).findOne({ where: {
|
|
986
|
+
realm: realm.name,
|
|
987
|
+
phoneNumber: { eq: data.phoneNumber }
|
|
988
|
+
} })) {
|
|
953
989
|
this.log.debug("Phone number already taken", { phoneNumber: data.phoneNumber });
|
|
954
990
|
throw new BadRequestError("User with this phone number already exists");
|
|
955
991
|
}
|
|
956
992
|
}
|
|
957
993
|
const user = await this.users(userRealmName).create({
|
|
958
994
|
...data,
|
|
959
|
-
roles: data.roles ??
|
|
995
|
+
roles: data.roles ?? realmSettings.defaultRoles,
|
|
960
996
|
realm: realm.name
|
|
961
997
|
});
|
|
962
998
|
this.log.info("User created", {
|
|
@@ -1011,6 +1047,8 @@ var UserService = class {
|
|
|
1011
1047
|
userRealmName
|
|
1012
1048
|
});
|
|
1013
1049
|
const user = await this.getUserById(id, userRealmName);
|
|
1050
|
+
await this.realmProvider.sessionRepository(userRealmName).deleteMany({ userId: { eq: id } });
|
|
1051
|
+
await this.realmProvider.identityRepository(userRealmName).deleteMany({ userId: { eq: id } });
|
|
1014
1052
|
await this.users(userRealmName).deleteById(id);
|
|
1015
1053
|
this.log.info("User deleted", { userId: id });
|
|
1016
1054
|
const realm = this.realmProvider.getRealm(userRealmName);
|
|
@@ -1282,6 +1320,16 @@ var CredentialService = class {
|
|
|
1282
1320
|
return this.realmProvider.identityRepository(userRealmName);
|
|
1283
1321
|
}
|
|
1284
1322
|
/**
|
|
1323
|
+
* Validate a password against the realm's password policy.
|
|
1324
|
+
*/
|
|
1325
|
+
validatePasswordPolicy(password, policy) {
|
|
1326
|
+
if (password.length < policy.minLength) throw new BadRequestError(`Password must be at least ${policy.minLength} characters`);
|
|
1327
|
+
if (policy.requireUppercase && !/[A-Z]/.test(password)) throw new BadRequestError("Password must contain at least one uppercase letter");
|
|
1328
|
+
if (policy.requireLowercase && !/[a-z]/.test(password)) throw new BadRequestError("Password must contain at least one lowercase letter");
|
|
1329
|
+
if (policy.requireNumbers && !/\d/.test(password)) throw new BadRequestError("Password must contain at least one number");
|
|
1330
|
+
if (policy.requireSpecialCharacters && !/[^a-zA-Z0-9]/.test(password)) throw new BadRequestError("Password must contain at least one special character");
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1285
1333
|
* Phase 1: Create a password reset intent.
|
|
1286
1334
|
*
|
|
1287
1335
|
* Validates the email, checks for existing user with credentials,
|
|
@@ -1298,6 +1346,13 @@ var CredentialService = class {
|
|
|
1298
1346
|
});
|
|
1299
1347
|
const intentId = randomUUID();
|
|
1300
1348
|
const expiresAt = this.dateTimeProvider.now().add(INTENT_TTL_MINUTES$1, "minutes").toISOString();
|
|
1349
|
+
if ((await this.realmProvider.getRealm(userRealmName).getSettings()).resetPasswordAllowed === false) {
|
|
1350
|
+
this.log.debug("Password reset not allowed for realm", { userRealmName });
|
|
1351
|
+
return {
|
|
1352
|
+
intentId,
|
|
1353
|
+
expiresAt
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1301
1356
|
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } });
|
|
1302
1357
|
if (!user) {
|
|
1303
1358
|
this.log.debug("Password reset requested for non-existent email", { email });
|
|
@@ -1369,6 +1424,9 @@ var CredentialService = class {
|
|
|
1369
1424
|
message: "Invalid or expired password reset intent"
|
|
1370
1425
|
});
|
|
1371
1426
|
}
|
|
1427
|
+
const realm = this.realmProvider.getRealm(intent.realmName);
|
|
1428
|
+
const realmSettings = await realm.getSettings();
|
|
1429
|
+
this.validatePasswordPolicy(body.newPassword, realmSettings.passwordPolicy);
|
|
1372
1430
|
if ((await this.verificationController.validateVerificationCode({
|
|
1373
1431
|
params: { type: "code" },
|
|
1374
1432
|
body: {
|
|
@@ -1396,7 +1454,6 @@ var CredentialService = class {
|
|
|
1396
1454
|
userId: intent.userId,
|
|
1397
1455
|
email: intent.email
|
|
1398
1456
|
});
|
|
1399
|
-
const realm = this.realmProvider.getRealm(intent.realmName);
|
|
1400
1457
|
await this.userAudits(intent.realmName)?.recordUser("update", {
|
|
1401
1458
|
userId: intent.userId,
|
|
1402
1459
|
userEmail: intent.email,
|
|
@@ -1447,6 +1504,9 @@ var CredentialService = class {
|
|
|
1447
1504
|
}).catch(() => {
|
|
1448
1505
|
throw new BadRequestError("Invalid or expired reset token");
|
|
1449
1506
|
})).alreadyVerified) throw new BadRequestError("Invalid or expired reset token");
|
|
1507
|
+
const realm = this.realmProvider.getRealm(userRealmName);
|
|
1508
|
+
const realmSettings = await realm.getSettings();
|
|
1509
|
+
this.validatePasswordPolicy(newPassword, realmSettings.passwordPolicy);
|
|
1450
1510
|
const user = await this.users(userRealmName).getOne({ where: { email: { eq: email } } });
|
|
1451
1511
|
const identity = await this.identities(userRealmName).getOne({ where: {
|
|
1452
1512
|
userId: { eq: user.id },
|
|
@@ -1455,7 +1515,6 @@ var CredentialService = class {
|
|
|
1455
1515
|
const hashedPassword = await this.cryptoProvider.hashPassword(newPassword);
|
|
1456
1516
|
await this.identities(userRealmName).updateById(identity.id, { password: hashedPassword });
|
|
1457
1517
|
await this.sessions(userRealmName).deleteMany({ userId: { eq: user.id } });
|
|
1458
|
-
const realm = this.realmProvider.getRealm(userRealmName);
|
|
1459
1518
|
await this.userAudits(userRealmName)?.recordUser("update", {
|
|
1460
1519
|
userId: user.id,
|
|
1461
1520
|
userEmail: email,
|
|
@@ -1484,10 +1543,15 @@ var RegistrationService = class {
|
|
|
1484
1543
|
cryptoProvider = $inject(CryptoProvider);
|
|
1485
1544
|
verificationController = $client();
|
|
1486
1545
|
realmProvider = $inject(RealmProvider);
|
|
1546
|
+
credentialService = $inject(CredentialService);
|
|
1487
1547
|
intentCache = $cache({
|
|
1488
1548
|
name: "api:users:registrations",
|
|
1489
1549
|
ttl: [INTENT_TTL_MINUTES, "minutes"]
|
|
1490
1550
|
});
|
|
1551
|
+
rateLimitCache = $cache({
|
|
1552
|
+
name: "api:users:registration-rate-limit",
|
|
1553
|
+
ttl: [15, "minutes"]
|
|
1554
|
+
});
|
|
1491
1555
|
userAudits(realmName) {
|
|
1492
1556
|
if (this.realmProvider.getRealm(realmName).features.audits) return this.alepha.inject(UserAudits);
|
|
1493
1557
|
}
|
|
@@ -1507,6 +1571,16 @@ var RegistrationService = class {
|
|
|
1507
1571
|
username: body.username,
|
|
1508
1572
|
userRealmName
|
|
1509
1573
|
});
|
|
1574
|
+
const request = this.alepha.store.get("alepha.http.request");
|
|
1575
|
+
const ipKey = request?.ip ? `register:ip:${request.ip}` : void 0;
|
|
1576
|
+
if (ipKey) {
|
|
1577
|
+
const count = await this.rateLimitCache.get(ipKey) ?? 0;
|
|
1578
|
+
if (count >= 10) {
|
|
1579
|
+
this.log.warn("Registration rate limit exceeded", { ip: request?.ip });
|
|
1580
|
+
throw new BadRequestError("Too many registration attempts, please try again later");
|
|
1581
|
+
}
|
|
1582
|
+
await this.rateLimitCache.set(ipKey, count + 1);
|
|
1583
|
+
}
|
|
1510
1584
|
const realmSettings = await this.realmProvider.getRealm(userRealmName).getSettings();
|
|
1511
1585
|
if (realmSettings?.registrationAllowed === false) {
|
|
1512
1586
|
this.log.warn("Registration not allowed for realm", { userRealmName });
|
|
@@ -1537,6 +1611,7 @@ var RegistrationService = class {
|
|
|
1537
1611
|
throw new BadRequestError("Phone number is required");
|
|
1538
1612
|
}
|
|
1539
1613
|
await this.checkUserAvailability(body, userRealmName);
|
|
1614
|
+
this.credentialService.validatePasswordPolicy(body.password, realmSettings.passwordPolicy);
|
|
1540
1615
|
const passwordHash = await this.cryptoProvider.hashPassword(body.password);
|
|
1541
1616
|
const requirements = {
|
|
1542
1617
|
email: realmSettings?.verifyEmailRequired === true && !!body.email,
|
|
@@ -1620,19 +1695,21 @@ var RegistrationService = class {
|
|
|
1620
1695
|
email: intent.data.email,
|
|
1621
1696
|
phoneNumber: intent.data.phoneNumber
|
|
1622
1697
|
}, userRealmName);
|
|
1623
|
-
|
|
1698
|
+
const realm = this.realmProvider.getRealm(userRealmName);
|
|
1699
|
+
const realmSettings = await realm.getSettings();
|
|
1624
1700
|
const user = await userRepository.create({
|
|
1625
|
-
realm:
|
|
1701
|
+
realm: realm.name,
|
|
1626
1702
|
username: intent.data.username,
|
|
1627
1703
|
email: intent.data.email,
|
|
1628
1704
|
phoneNumber: intent.data.phoneNumber,
|
|
1629
1705
|
firstName: intent.data.firstName,
|
|
1630
1706
|
lastName: intent.data.lastName,
|
|
1631
1707
|
picture: intent.data.picture,
|
|
1632
|
-
roles:
|
|
1708
|
+
roles: realmSettings.defaultRoles,
|
|
1633
1709
|
enabled: true,
|
|
1634
1710
|
emailVerified: intent.requirements.email
|
|
1635
1711
|
});
|
|
1712
|
+
await this.intentCache.invalidate(body.intentId);
|
|
1636
1713
|
await identityRepository.create({
|
|
1637
1714
|
userId: user.id,
|
|
1638
1715
|
provider: "credentials",
|
|
@@ -1643,7 +1720,6 @@ var RegistrationService = class {
|
|
|
1643
1720
|
email: user.email,
|
|
1644
1721
|
username: user.username
|
|
1645
1722
|
});
|
|
1646
|
-
const realm = this.realmProvider.getRealm(userRealmName);
|
|
1647
1723
|
await this.userAudits(userRealmName)?.recordUser("create", {
|
|
1648
1724
|
userId: user.id,
|
|
1649
1725
|
userEmail: user.email ?? void 0,
|
|
@@ -1663,21 +1739,31 @@ var RegistrationService = class {
|
|
|
1663
1739
|
* Check if username, email, and phone are available.
|
|
1664
1740
|
*/
|
|
1665
1741
|
async checkUserAvailability(body, userRealmName) {
|
|
1742
|
+
const realm = this.realmProvider.getRealm(userRealmName);
|
|
1666
1743
|
const userRepository = this.realmProvider.userRepository(userRealmName);
|
|
1667
1744
|
if (body.username) {
|
|
1668
|
-
if (await userRepository.findOne({ where: {
|
|
1745
|
+
if (await userRepository.findOne({ where: {
|
|
1746
|
+
realm: realm.name,
|
|
1747
|
+
username: { ilike: body.username }
|
|
1748
|
+
} })) {
|
|
1669
1749
|
this.log.debug("Username already taken", { username: body.username });
|
|
1670
1750
|
throw new ConflictError("User with this username already exists");
|
|
1671
1751
|
}
|
|
1672
1752
|
}
|
|
1673
1753
|
if (body.email) {
|
|
1674
|
-
if (await userRepository.findOne({ where: {
|
|
1754
|
+
if (await userRepository.findOne({ where: {
|
|
1755
|
+
realm: realm.name,
|
|
1756
|
+
email: { eq: body.email }
|
|
1757
|
+
} })) {
|
|
1675
1758
|
this.log.debug("Email already taken", { email: body.email });
|
|
1676
1759
|
throw new ConflictError("User with this email already exists");
|
|
1677
1760
|
}
|
|
1678
1761
|
}
|
|
1679
1762
|
if (body.phoneNumber) {
|
|
1680
|
-
if (await userRepository.findOne({ where: {
|
|
1763
|
+
if (await userRepository.findOne({ where: {
|
|
1764
|
+
realm: realm.name,
|
|
1765
|
+
phoneNumber: { eq: body.phoneNumber }
|
|
1766
|
+
} })) {
|
|
1681
1767
|
this.log.debug("Phone number already taken", { phoneNumber: body.phoneNumber });
|
|
1682
1768
|
throw new ConflictError("User with this phone number already exists");
|
|
1683
1769
|
}
|
|
@@ -1924,9 +2010,8 @@ var UserController = class {
|
|
|
1924
2010
|
userRealmName: t.optional(t.string()),
|
|
1925
2011
|
method: t.optional(t.enum(["code", "link"], {
|
|
1926
2012
|
default: "code",
|
|
1927
|
-
description: "Verification method: \"code\" sends a 6-digit code, \"link\" sends a clickable verification link."
|
|
1928
|
-
}))
|
|
1929
|
-
verifyUrl: t.optional(t.string({ description: "Base URL for verification link. Required when method is \"link\". Token and email will be appended as query params." }))
|
|
2013
|
+
description: "Verification method: \"code\" sends a 6-digit code, \"link\" sends a clickable verification link. When using \"link\", configure verifyEmailUrl in realm settings."
|
|
2014
|
+
}))
|
|
1930
2015
|
}),
|
|
1931
2016
|
body: t.object({ email: t.email() }),
|
|
1932
2017
|
response: t.object({
|
|
@@ -1936,7 +2021,7 @@ var UserController = class {
|
|
|
1936
2021
|
},
|
|
1937
2022
|
handler: async ({ body, query }) => {
|
|
1938
2023
|
const method = query.method ?? "code";
|
|
1939
|
-
await this.userService.requestEmailVerification(body.email, query.userRealmName, method
|
|
2024
|
+
await this.userService.requestEmailVerification(body.email, query.userRealmName, method);
|
|
1940
2025
|
return {
|
|
1941
2026
|
success: true,
|
|
1942
2027
|
message: method === "link" ? "If an account exists with this email, a verification link has been sent." : "If an account exists with this email, a verification code has been sent."
|
|
@@ -1975,6 +2060,7 @@ var UserController = class {
|
|
|
1975
2060
|
checkEmailVerification = $action({
|
|
1976
2061
|
path: "/users/email-verification/check",
|
|
1977
2062
|
group: this.group,
|
|
2063
|
+
use: [$secure()],
|
|
1978
2064
|
schema: {
|
|
1979
2065
|
query: t.object({
|
|
1980
2066
|
email: t.email(),
|
|
@@ -2039,6 +2125,9 @@ var SessionService = class SessionService {
|
|
|
2039
2125
|
userAudits(realmName) {
|
|
2040
2126
|
if (this.realmProvider.getRealm(realmName).features.audits) return this.alepha.inject(UserAudits);
|
|
2041
2127
|
}
|
|
2128
|
+
userNotifications(realmName) {
|
|
2129
|
+
if (this.realmProvider.getRealm(realmName).features.notifications) return this.alepha.inject(UserNotifications);
|
|
2130
|
+
}
|
|
2042
2131
|
users(userRealmName) {
|
|
2043
2132
|
return this.realmProvider.userRepository(userRealmName);
|
|
2044
2133
|
}
|
|
@@ -2103,10 +2192,7 @@ var SessionService = class SessionService {
|
|
|
2103
2192
|
if (candidate.length < 3) candidate = `user${candidate}`;
|
|
2104
2193
|
candidate = candidate.slice(0, maxLength - 2);
|
|
2105
2194
|
const isAvailable = async (name) => {
|
|
2106
|
-
return !
|
|
2107
|
-
where: { username: { contains: name } },
|
|
2108
|
-
limit: 1
|
|
2109
|
-
})).some((u) => u.username?.toLowerCase() === name.toLowerCase());
|
|
2195
|
+
return !await users.findOne({ where: { username: { ilike: name } } });
|
|
2110
2196
|
};
|
|
2111
2197
|
if (await isAvailable(candidate)) return candidate;
|
|
2112
2198
|
for (let i = 2; i <= maxSuffixAttempts + 1; i++) {
|
|
@@ -2240,6 +2326,22 @@ var SessionService = class SessionService {
|
|
|
2240
2326
|
}
|
|
2241
2327
|
throw new InvalidCredentialsError();
|
|
2242
2328
|
}
|
|
2329
|
+
if (!user.enabled) {
|
|
2330
|
+
this.log.warn("Login attempt for disabled account", {
|
|
2331
|
+
userId: user.id,
|
|
2332
|
+
realm: name
|
|
2333
|
+
});
|
|
2334
|
+
await this.userAudits(userRealmName)?.recordAuth("login_failed", {
|
|
2335
|
+
userRealm: name,
|
|
2336
|
+
resourceId: user.id,
|
|
2337
|
+
description: "Login attempt for disabled account",
|
|
2338
|
+
metadata: {
|
|
2339
|
+
provider,
|
|
2340
|
+
username
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
throw new InvalidCredentialsError();
|
|
2344
|
+
}
|
|
2243
2345
|
const accountKey = `login:account:${name}:${user.id}`;
|
|
2244
2346
|
if (await this.isLoginLocked(accountKey, loginRateLimit.accountMaxAttempts)) {
|
|
2245
2347
|
this.log.warn("Login blocked — account rate limit exceeded", {
|
|
@@ -2285,13 +2387,25 @@ var SessionService = class SessionService {
|
|
|
2285
2387
|
metadata: { ip: request?.ip }
|
|
2286
2388
|
});
|
|
2287
2389
|
}
|
|
2288
|
-
if (await this.recordFailedLogin(accountKey, loginRateLimit.accountMaxAttempts, loginRateLimit.windowMs))
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2390
|
+
if (await this.recordFailedLogin(accountKey, loginRateLimit.accountMaxAttempts, loginRateLimit.windowMs)) {
|
|
2391
|
+
await this.userAudits(userRealmName)?.record("security", "rate_limited", {
|
|
2392
|
+
userRealm: name,
|
|
2393
|
+
resourceId: user.id,
|
|
2394
|
+
success: false,
|
|
2395
|
+
description: "Account temporarily locked due to too many failed login attempts",
|
|
2396
|
+
metadata: { userId: user.id }
|
|
2397
|
+
});
|
|
2398
|
+
if (user.email) {
|
|
2399
|
+
const lockoutMinutes = Math.round(loginRateLimit.windowMs / 6e4);
|
|
2400
|
+
await this.userNotifications(userRealmName)?.accountLockout.push({
|
|
2401
|
+
contact: user.email,
|
|
2402
|
+
variables: {
|
|
2403
|
+
email: user.email,
|
|
2404
|
+
lockoutMinutes
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2295
2409
|
throw new InvalidCredentialsError();
|
|
2296
2410
|
}
|
|
2297
2411
|
await this.userAudits(userRealmName)?.recordAuth("login", {
|
|
@@ -2352,6 +2466,14 @@ var SessionService = class SessionService {
|
|
|
2352
2466
|
throw new UnauthorizedError("Session expired");
|
|
2353
2467
|
}
|
|
2354
2468
|
const user = await this.users(userRealmName).getOne({ where: { id: { eq: session.userId } } });
|
|
2469
|
+
if (!user.enabled) {
|
|
2470
|
+
this.log.warn("Session refresh for disabled account", {
|
|
2471
|
+
userId: user.id,
|
|
2472
|
+
sessionId: session.id
|
|
2473
|
+
});
|
|
2474
|
+
await this.sessions(userRealmName).deleteById(session.id);
|
|
2475
|
+
throw new UnauthorizedError("Account disabled");
|
|
2476
|
+
}
|
|
2355
2477
|
await this.ensureAdminRole(user, userRealmName);
|
|
2356
2478
|
this.log.debug("Session refreshed", {
|
|
2357
2479
|
sessionId: session.id,
|
|
@@ -2422,8 +2544,19 @@ var SessionService = class SessionService {
|
|
|
2422
2544
|
...profile
|
|
2423
2545
|
};
|
|
2424
2546
|
}
|
|
2425
|
-
const existing = await users.findOne({ where: {
|
|
2547
|
+
const existing = await users.findOne({ where: {
|
|
2548
|
+
realm: realm.name,
|
|
2549
|
+
email: profile.email
|
|
2550
|
+
} });
|
|
2426
2551
|
if (existing) {
|
|
2552
|
+
if (profile.email_verified === false) {
|
|
2553
|
+
this.log.warn("OAuth2 profile email not verified by provider, refusing auto-link", {
|
|
2554
|
+
provider,
|
|
2555
|
+
email: profile.email,
|
|
2556
|
+
userId: existing.id
|
|
2557
|
+
});
|
|
2558
|
+
throw new BadRequestError("Cannot link account: email not verified by provider");
|
|
2559
|
+
}
|
|
2427
2560
|
this.log.debug("Linking OAuth2 profile to existing user by email", {
|
|
2428
2561
|
provider,
|
|
2429
2562
|
profileSub: profile.sub,
|
|
@@ -2466,7 +2599,7 @@ var SessionService = class SessionService {
|
|
|
2466
2599
|
username,
|
|
2467
2600
|
email: profile.email,
|
|
2468
2601
|
emailVerified: true,
|
|
2469
|
-
roles:
|
|
2602
|
+
roles: realmSettings.defaultRoles
|
|
2470
2603
|
});
|
|
2471
2604
|
if (profile.picture) {
|
|
2472
2605
|
this.log.debug("Fetching user profile picture from OAuth2 provider", {
|
|
@@ -2645,7 +2778,7 @@ const loginSchema = t.object({
|
|
|
2645
2778
|
description: "Username or email address for login"
|
|
2646
2779
|
}),
|
|
2647
2780
|
password: t.text({
|
|
2648
|
-
minLength:
|
|
2781
|
+
minLength: 8,
|
|
2649
2782
|
description: "User password"
|
|
2650
2783
|
})
|
|
2651
2784
|
});
|