@actuate-media/cms-core 0.38.0 → 0.40.0
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/__tests__/api/ai-status.test.d.ts +2 -0
- package/dist/__tests__/api/ai-status.test.d.ts.map +1 -0
- package/dist/__tests__/api/ai-status.test.js +110 -0
- package/dist/__tests__/api/ai-status.test.js.map +1 -0
- package/dist/__tests__/api/users-detail.test.d.ts +2 -0
- package/dist/__tests__/api/users-detail.test.d.ts.map +1 -0
- package/dist/__tests__/api/users-detail.test.js +349 -0
- package/dist/__tests__/api/users-detail.test.js.map +1 -0
- package/dist/__tests__/api/users-invite.test.d.ts +2 -0
- package/dist/__tests__/api/users-invite.test.d.ts.map +1 -0
- package/dist/__tests__/api/users-invite.test.js +221 -0
- package/dist/__tests__/api/users-invite.test.js.map +1 -0
- package/dist/__tests__/auth/invites.test.d.ts +2 -0
- package/dist/__tests__/auth/invites.test.d.ts.map +1 -0
- package/dist/__tests__/auth/invites.test.js +181 -0
- package/dist/__tests__/auth/invites.test.js.map +1 -0
- package/dist/__tests__/auth/reset.test.js +52 -1
- package/dist/__tests__/auth/reset.test.js.map +1 -1
- package/dist/__tests__/auth/roles.test.d.ts +2 -0
- package/dist/__tests__/auth/roles.test.d.ts.map +1 -0
- package/dist/__tests__/auth/roles.test.js +60 -0
- package/dist/__tests__/auth/roles.test.js.map +1 -0
- package/dist/__tests__/auth/team.test.d.ts +2 -0
- package/dist/__tests__/auth/team.test.d.ts.map +1 -0
- package/dist/__tests__/auth/team.test.js +139 -0
- package/dist/__tests__/auth/team.test.js.map +1 -0
- package/dist/__tests__/auth/user-admin.test.d.ts +2 -0
- package/dist/__tests__/auth/user-admin.test.d.ts.map +1 -0
- package/dist/__tests__/auth/user-admin.test.js +162 -0
- package/dist/__tests__/auth/user-admin.test.js.map +1 -0
- package/dist/__tests__/email/sender.test.d.ts +2 -0
- package/dist/__tests__/email/sender.test.d.ts.map +1 -0
- package/dist/__tests__/email/sender.test.js +33 -0
- package/dist/__tests__/email/sender.test.js.map +1 -0
- package/dist/__tests__/middleware.test.js +30 -0
- package/dist/__tests__/middleware.test.js.map +1 -1
- package/dist/__tests__/setup/index.test.js +37 -2
- package/dist/__tests__/setup/index.test.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +1116 -194
- package/dist/api/handlers.js.map +1 -1
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/invite-email.d.ts +12 -0
- package/dist/auth/invite-email.d.ts.map +1 -0
- package/dist/auth/invite-email.js +57 -0
- package/dist/auth/invite-email.js.map +1 -0
- package/dist/auth/invites.d.ts +71 -0
- package/dist/auth/invites.d.ts.map +1 -0
- package/dist/auth/invites.js +279 -0
- package/dist/auth/invites.js.map +1 -0
- package/dist/auth/reset.d.ts +6 -0
- package/dist/auth/reset.d.ts.map +1 -1
- package/dist/auth/reset.js +38 -7
- package/dist/auth/reset.js.map +1 -1
- package/dist/auth/roles.d.ts +51 -0
- package/dist/auth/roles.d.ts.map +1 -0
- package/dist/auth/roles.js +159 -0
- package/dist/auth/roles.js.map +1 -0
- package/dist/auth/team.d.ts +86 -0
- package/dist/auth/team.d.ts.map +1 -0
- package/dist/auth/team.js +270 -0
- package/dist/auth/team.js.map +1 -0
- package/dist/auth/user-admin.d.ts +91 -0
- package/dist/auth/user-admin.d.ts.map +1 -0
- package/dist/auth/user-admin.js +250 -0
- package/dist/auth/user-admin.js.map +1 -0
- package/dist/email/sender.d.ts +35 -0
- package/dist/email/sender.d.ts.map +1 -0
- package/dist/email/sender.js +25 -0
- package/dist/email/sender.js.map +1 -0
- package/dist/forms/notify.d.ts.map +1 -1
- package/dist/forms/notify.js +3 -2
- package/dist/forms/notify.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +60 -1
- package/dist/middleware.js.map +1 -1
- package/dist/setup/index.d.ts +14 -0
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/index.js +29 -3
- package/dist/setup/index.js.map +1 -1
- package/package.json +11 -1
- package/prisma/migrations/0011_user_roles_owner_viewer/migration.sql +6 -0
- package/prisma/migrations/0012_invites_and_user_lifecycle/migration.sql +40 -0
- package/prisma/schema.prisma +43 -0
package/dist/auth/index.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export type { SessionPayload, SessionOptions } from './session.js';
|
|
|
3
3
|
export { hashPassword, verifyPassword, validatePasswordPolicy, checkPasswordBreach, } from './password.js';
|
|
4
4
|
export { generateResetToken, hashToken, createPasswordReset, executePasswordReset, } from './reset.js';
|
|
5
5
|
export type { ResetToken } from './reset.js';
|
|
6
|
+
export { createInvite, listInvites, resendInvite, cancelInvite, updateInviteRole, extendInvite, expireStaleInvites, markInviteAcceptedForEmail, INVITE_EXPIRY_MS, VALID_EXPIRY_DAYS, } from './invites.js';
|
|
7
|
+
export type { CreateInviteInput, InviteResult, SerializedInvite, InviteConfig, ListInvitesParams, } from './invites.js';
|
|
8
|
+
export { ASSIGNABLE_ROLES, DISPLAY_ROLES, toDisplayRole, normalizeAssignableRole, enumRolesForDisplay, isOwnerRole, roleRank, permissionsForRole, } from './roles.js';
|
|
9
|
+
export type { UserRoleEnum, DisplayRole, Permission } from './roles.js';
|
|
6
10
|
export { generateTOTPSecret, verifyTOTP, generateBackupCodes, generateTOTPUri } from './totp.js';
|
|
7
11
|
export { initiateOAuth, handleCallback, linkAccount } from './oauth.js';
|
|
8
12
|
export { generateCodeVerifier, generateCodeChallenge, getAuthorizationUrl, exchangeCodeForTokens, getUserProfile, handleOAuthCallback, } from './oauth.js';
|
package/dist/auth/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC1F,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElE,OAAO,EACL,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhG,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC1F,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElE,OAAO,EACL,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,0BAA0B,EAC1B,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,cAAc,CAAA;AACrB,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,GAClB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,uBAAuB,EACvB,mBAAmB,EACnB,WAAW,EACX,QAAQ,EACR,kBAAkB,GACnB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEvE,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhG,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA"}
|
package/dist/auth/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { createSession, verifySession, revokeSession, refreshSession } from './session.js';
|
|
2
2
|
export { hashPassword, verifyPassword, validatePasswordPolicy, checkPasswordBreach, } from './password.js';
|
|
3
3
|
export { generateResetToken, hashToken, createPasswordReset, executePasswordReset, } from './reset.js';
|
|
4
|
+
export { createInvite, listInvites, resendInvite, cancelInvite, updateInviteRole, extendInvite, expireStaleInvites, markInviteAcceptedForEmail, INVITE_EXPIRY_MS, VALID_EXPIRY_DAYS, } from './invites.js';
|
|
5
|
+
export { ASSIGNABLE_ROLES, DISPLAY_ROLES, toDisplayRole, normalizeAssignableRole, enumRolesForDisplay, isOwnerRole, roleRank, permissionsForRole, } from './roles.js';
|
|
4
6
|
export { generateTOTPSecret, verifyTOTP, generateBackupCodes, generateTOTPUri } from './totp.js';
|
|
5
7
|
export { initiateOAuth, handleCallback, linkAccount } from './oauth.js';
|
|
6
8
|
export { generateCodeVerifier, generateCodeChallenge, getAuthorizationUrl, exchangeCodeForTokens, getUserProfile, handleOAuthCallback, } from './oauth.js';
|
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAG1F,OAAO,EACL,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhG,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAA;AASnB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAG1F,OAAO,EACL,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AAGnB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,0BAA0B,EAC1B,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAQrB,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,uBAAuB,EACvB,mBAAmB,EACnB,WAAW,EACX,QAAQ,EACR,kBAAkB,GACnB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhG,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAA;AASnB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function userInvitationEmailTemplate(args: {
|
|
2
|
+
inviteUrl: string;
|
|
3
|
+
userName: string;
|
|
4
|
+
role: string;
|
|
5
|
+
invitedByName?: string;
|
|
6
|
+
siteName?: string;
|
|
7
|
+
}): {
|
|
8
|
+
subject: string;
|
|
9
|
+
html: string;
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invite-email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invite-email.d.ts","sourceRoot":"","sources":["../../src/auth/invite-email.ts"],"names":[],"mappings":"AAqBA,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAqClD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User invitation email — a one-click link to accept the invite and set a
|
|
3
|
+
* password. The link points at the same `/reset-password` page the password
|
|
4
|
+
* reset flow uses (the invitee is "resetting" from an unusable random password
|
|
5
|
+
* to one they choose), so no separate accept page is required.
|
|
6
|
+
*/
|
|
7
|
+
function escapeHtml(value) {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>')
|
|
12
|
+
.replace(/"/g, '"')
|
|
13
|
+
.replace(/'/g, ''');
|
|
14
|
+
}
|
|
15
|
+
/** Human-friendly role label for the email body. */
|
|
16
|
+
function roleLabel(role) {
|
|
17
|
+
const lower = role.toLowerCase();
|
|
18
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
19
|
+
}
|
|
20
|
+
export function userInvitationEmailTemplate(args) {
|
|
21
|
+
const site = args.siteName?.trim() || 'Actuate CMS';
|
|
22
|
+
const inviter = args.invitedByName?.trim();
|
|
23
|
+
const role = roleLabel(args.role);
|
|
24
|
+
const safeSite = escapeHtml(site);
|
|
25
|
+
const safeName = escapeHtml(args.userName);
|
|
26
|
+
const safeRole = escapeHtml(role);
|
|
27
|
+
const invitedLine = inviter
|
|
28
|
+
? `${escapeHtml(inviter)} invited you to join ${safeSite}`
|
|
29
|
+
: `You've been invited to join ${safeSite}`;
|
|
30
|
+
return {
|
|
31
|
+
subject: `You're invited to ${site}`,
|
|
32
|
+
html: `<!DOCTYPE html>
|
|
33
|
+
<html>
|
|
34
|
+
<head><meta charset="utf-8"></head>
|
|
35
|
+
<body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f9fafb;">
|
|
36
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;margin:40px auto;background:#fff;border-radius:8px;border:1px solid #e5e7eb;">
|
|
37
|
+
<tr>
|
|
38
|
+
<td style="padding:32px;">
|
|
39
|
+
<h1 style="margin:0 0 16px;font-size:20px;color:#111827;">${invitedLine}</h1>
|
|
40
|
+
<p style="margin:0 0 24px;font-size:15px;line-height:1.6;color:#374151;">
|
|
41
|
+
Hi ${safeName}, you've been added as a <strong>${safeRole}</strong>. Click the button below to set your password and sign in.
|
|
42
|
+
</p>
|
|
43
|
+
<a href="${args.inviteUrl}" style="display:inline-block;padding:12px 24px;background:#3b82f6;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;font-size:14px;">
|
|
44
|
+
Accept invitation
|
|
45
|
+
</a>
|
|
46
|
+
<p style="margin:24px 0 0;font-size:13px;color:#9ca3af;">
|
|
47
|
+
If you weren't expecting this, you can safely ignore this email. This invitation link expires in 7 days.
|
|
48
|
+
</p>
|
|
49
|
+
</td>
|
|
50
|
+
</tr>
|
|
51
|
+
</table>
|
|
52
|
+
</body>
|
|
53
|
+
</html>`,
|
|
54
|
+
text: `${inviter ? `${inviter} invited you` : "You've been invited"} to join ${site} as a ${role}.\n\nHi ${args.userName}, set your password and sign in using the link below:\n\n${args.inviteUrl}\n\nIf you weren't expecting this, you can safely ignore this email. This invitation link expires in 7 days.`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=invite-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invite-email.js","sourceRoot":"","sources":["../../src/auth/invite-email.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC3B,CAAC;AAED,oDAAoD;AACpD,SAAS,SAAS,CAAC,IAAY;IAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;IAChC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACvD,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,IAM3C;IACC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,aAAa,CAAA;IACnD,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,CAAA;IAC1C,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC1C,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,WAAW,GAAG,OAAO;QACzB,CAAC,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,wBAAwB,QAAQ,EAAE;QAC1D,CAAC,CAAC,+BAA+B,QAAQ,EAAE,CAAA;IAE7C,OAAO;QACL,OAAO,EAAE,qBAAqB,IAAI,EAAE;QACpC,IAAI,EAAE;;;;;;;oEAO0D,WAAW;;eAEhE,QAAQ,oCAAoC,QAAQ;;mBAEhD,IAAI,CAAC,SAAS;;;;;;;;;;QAUzB;QACJ,IAAI,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,cAAc,CAAC,CAAC,CAAC,qBAAqB,YAAY,IAAI,SAAS,IAAI,WAAW,IAAI,CAAC,QAAQ,4DAA4D,IAAI,CAAC,SAAS,8GAA8G;KACjT,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type UserRoleEnum } from './roles.js';
|
|
2
|
+
/** Invitations are valid for a week by default — a person has to act. */
|
|
3
|
+
export declare const INVITE_EXPIRY_MS: number;
|
|
4
|
+
export declare const VALID_EXPIRY_DAYS: readonly [7, 14, 30];
|
|
5
|
+
export interface InviteConfig {
|
|
6
|
+
siteUrl: string;
|
|
7
|
+
/** Admin mount path; the accept link is `<siteUrl><adminPath>/reset-password`. */
|
|
8
|
+
adminPath?: string;
|
|
9
|
+
invitedByName?: string;
|
|
10
|
+
invitedByUserId?: string;
|
|
11
|
+
siteName?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CreateInviteInput {
|
|
14
|
+
email: string;
|
|
15
|
+
role: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
/** Days until expiry; one of {@link VALID_EXPIRY_DAYS}. Defaults to 7. */
|
|
18
|
+
expiresInDays?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface InviteResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
/** Distinguish "already invited" so the UI can offer Resend instead. */
|
|
24
|
+
code?: 'DUPLICATE_USER' | 'PENDING_INVITE' | 'INVALID_EMAIL' | 'INVALID_ROLE' | 'NOT_FOUND';
|
|
25
|
+
/** The accept link. Returned only when email was NOT sent, so an admin can share it. */
|
|
26
|
+
inviteUrl?: string;
|
|
27
|
+
emailed?: boolean;
|
|
28
|
+
invite?: SerializedInvite;
|
|
29
|
+
}
|
|
30
|
+
export interface SerializedInvite {
|
|
31
|
+
id: string;
|
|
32
|
+
email: string;
|
|
33
|
+
role: UserRoleEnum;
|
|
34
|
+
roleName: string;
|
|
35
|
+
status: string;
|
|
36
|
+
invitedByUserId: string | null;
|
|
37
|
+
expiresAt: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
expired: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create an invitation. Provisions a placeholder user, persists a PENDING
|
|
43
|
+
* invite + single-use reset token, and emails the accept link.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createInvite(db: any, input: CreateInviteInput, config: InviteConfig): Promise<InviteResult>;
|
|
46
|
+
export interface ListInvitesParams {
|
|
47
|
+
status?: string;
|
|
48
|
+
search?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* List invites, lazily flipping any past-expiry PENDING rows to EXPIRED so the
|
|
52
|
+
* status is always honest without a background job.
|
|
53
|
+
*/
|
|
54
|
+
export declare function listInvites(db: any, params?: ListInvitesParams): Promise<SerializedInvite[]>;
|
|
55
|
+
/** Flip PENDING invites whose expiry has passed to EXPIRED. Returns the count. */
|
|
56
|
+
export declare function expireStaleInvites(db: any): Promise<number>;
|
|
57
|
+
/** Resend (or revive an expired) invite: fresh token, reset expiry, re-email. */
|
|
58
|
+
export declare function resendInvite(db: any, inviteId: string, config: InviteConfig): Promise<InviteResult>;
|
|
59
|
+
/** Cancel an invite and deactivate its placeholder user so the link is dead. */
|
|
60
|
+
export declare function cancelInvite(db: any, inviteId: string): Promise<InviteResult>;
|
|
61
|
+
/** Change the role on a pending invite (and its placeholder user). */
|
|
62
|
+
export declare function updateInviteRole(db: any, inviteId: string, role: string): Promise<InviteResult>;
|
|
63
|
+
/** Extend a pending invite's expiry and re-sync its reset token expiry. */
|
|
64
|
+
export declare function extendInvite(db: any, inviteId: string, days: number): Promise<InviteResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Mark the pending invite for an email as ACCEPTED. Called when a user sets
|
|
67
|
+
* their password through the reset/accept flow. Safe to call for non-invited
|
|
68
|
+
* users — it simply updates zero rows.
|
|
69
|
+
*/
|
|
70
|
+
export declare function markInviteAcceptedForEmail(db: any, email: string, userId: string): Promise<boolean>;
|
|
71
|
+
//# sourceMappingURL=invites.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invites.d.ts","sourceRoot":"","sources":["../../src/auth/invites.ts"],"names":[],"mappings":"AAoBA,OAAO,EAA0C,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAEtF,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,QAA0B,CAAA;AACvD,eAAO,MAAM,iBAAiB,sBAAuB,CAAA;AAIrD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,IAAI,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,eAAe,GAAG,cAAc,GAAG,WAAW,CAAA;IAC3F,wFAAwF;IACxF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,gBAAgB,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,YAAY,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;CACjB;AAqDD;;;GAGG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,GAAG,EACP,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAgFvB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,GAAG,EACP,MAAM,GAAE,iBAAsB,GAC7B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAS7B;AAED,kFAAkF;AAClF,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjE;AAED,iFAAiF;AACjF,wBAAsB,YAAY,CAChC,EAAE,EAAE,GAAG,EACP,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA6CvB;AAED,gFAAgF;AAChF,wBAAsB,YAAY,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAuBnF;AAED,sEAAsE;AACtE,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,GAAG,EACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAgBvB;AAED,2EAA2E;AAC3E,wBAAsB,YAAY,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAkBjG;AAED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,GAAG,EACP,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAOlB"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invite lifecycle service (backed by the `actuate_invites` table).
|
|
3
|
+
*
|
|
4
|
+
* The `Invite` row is the authoritative record of an invitation — it drives the
|
|
5
|
+
* Users admin UI (pending list, resend/cancel/extend/change-role) and seat
|
|
6
|
+
* accounting. To keep a working, secure acceptance flow without a separate
|
|
7
|
+
* invitee page, creating an invite also provisions a placeholder user with an
|
|
8
|
+
* unusable password (the proven Batch-2 mechanism) and issues a single-use,
|
|
9
|
+
* hashed password-reset token. The invitee sets their password via the existing
|
|
10
|
+
* `/reset-password` page; completing that flips the invite to ACCEPTED (see
|
|
11
|
+
* `markInviteAcceptedForEmail`, called from `executePasswordReset`).
|
|
12
|
+
*
|
|
13
|
+
* Members are de-duplicated against pending invites by email so a not-yet-
|
|
14
|
+
* accepted invite is shown only as a pending row, never double-counted.
|
|
15
|
+
*/
|
|
16
|
+
import { randomBytes } from 'node:crypto';
|
|
17
|
+
import { hashPassword } from './password.js';
|
|
18
|
+
import { generateResetToken } from './reset.js';
|
|
19
|
+
import { userInvitationEmailTemplate } from './invite-email.js';
|
|
20
|
+
import { resolveEmailSender } from '../email/sender.js';
|
|
21
|
+
import { normalizeAssignableRole, toDisplayRole } from './roles.js';
|
|
22
|
+
/** Invitations are valid for a week by default — a person has to act. */
|
|
23
|
+
export const INVITE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
24
|
+
export const VALID_EXPIRY_DAYS = [7, 14, 30];
|
|
25
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
26
|
+
function expiryDays(input) {
|
|
27
|
+
const v = input ?? 7;
|
|
28
|
+
return VALID_EXPIRY_DAYS.includes(v) ? v : 7;
|
|
29
|
+
}
|
|
30
|
+
function buildAcceptUrl(rawToken, config) {
|
|
31
|
+
const base = config.siteUrl.replace(/\/$/, '');
|
|
32
|
+
const adminPath = (config.adminPath ?? '/admin').replace(/\/$/, '');
|
|
33
|
+
return `${base}${adminPath}/reset-password?token=${rawToken}`;
|
|
34
|
+
}
|
|
35
|
+
function serializeInvite(row) {
|
|
36
|
+
return {
|
|
37
|
+
id: row.id,
|
|
38
|
+
email: row.email,
|
|
39
|
+
role: row.role,
|
|
40
|
+
roleName: toDisplayRole(row.role),
|
|
41
|
+
status: row.status,
|
|
42
|
+
invitedByUserId: row.invitedByUserId ?? null,
|
|
43
|
+
expiresAt: row.expiresAt instanceof Date ? row.expiresAt.toISOString() : row.expiresAt,
|
|
44
|
+
createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : row.createdAt,
|
|
45
|
+
expired: new Date(row.expiresAt).getTime() < Date.now(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function sendInviteEmail(email, name, role, inviteUrl, config) {
|
|
49
|
+
const sender = resolveEmailSender();
|
|
50
|
+
if (!sender)
|
|
51
|
+
return false;
|
|
52
|
+
const tpl = userInvitationEmailTemplate({
|
|
53
|
+
inviteUrl,
|
|
54
|
+
userName: name,
|
|
55
|
+
role,
|
|
56
|
+
invitedByName: config.invitedByName,
|
|
57
|
+
siteName: config.siteName,
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
await sender.send({ to: email, subject: tpl.subject, html: tpl.html, text: tpl.text });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Best-effort: the invite + token are persisted, so the admin can share the
|
|
65
|
+
// returned link manually. Don't fail the whole invite on a send error.
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create an invitation. Provisions a placeholder user, persists a PENDING
|
|
71
|
+
* invite + single-use reset token, and emails the accept link.
|
|
72
|
+
*/
|
|
73
|
+
export async function createInvite(db, input, config) {
|
|
74
|
+
const email = input.email.toLowerCase().trim();
|
|
75
|
+
if (!EMAIL_RE.test(email)) {
|
|
76
|
+
return { success: false, error: 'Enter a valid email address.', code: 'INVALID_EMAIL' };
|
|
77
|
+
}
|
|
78
|
+
const role = normalizeAssignableRole(input.role);
|
|
79
|
+
if (!role) {
|
|
80
|
+
return { success: false, error: 'Select a valid role.', code: 'INVALID_ROLE' };
|
|
81
|
+
}
|
|
82
|
+
// Already a pending invite? Offer resend rather than a duplicate.
|
|
83
|
+
const pending = await db.invite.findFirst({
|
|
84
|
+
where: { email: { equals: email, mode: 'insensitive' }, status: 'PENDING' },
|
|
85
|
+
});
|
|
86
|
+
if (pending) {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: 'This person already has a pending invitation. Resend it instead.',
|
|
90
|
+
code: 'PENDING_INVITE',
|
|
91
|
+
invite: serializeInvite(pending),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// An existing, already-accepted user can't be re-invited.
|
|
95
|
+
const existingUser = await db.user.findFirst({
|
|
96
|
+
where: { email: { equals: email, mode: 'insensitive' } },
|
|
97
|
+
});
|
|
98
|
+
if (existingUser) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: 'A user with this email already exists.',
|
|
102
|
+
code: 'DUPLICATE_USER',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const name = email.split('@')[0] || email;
|
|
106
|
+
const passwordHash = await hashPassword(randomBytes(32).toString('hex'));
|
|
107
|
+
const expiresAt = new Date(Date.now() + expiryDays(input.expiresInDays) * 24 * 60 * 60 * 1000);
|
|
108
|
+
// Placeholder user — unusable password, not yet approved/accepted.
|
|
109
|
+
const user = await db.user.create({
|
|
110
|
+
data: {
|
|
111
|
+
email,
|
|
112
|
+
name,
|
|
113
|
+
role,
|
|
114
|
+
passwordHash,
|
|
115
|
+
isActive: true,
|
|
116
|
+
isApproved: false,
|
|
117
|
+
emailVerified: false,
|
|
118
|
+
invitedById: config.invitedByUserId ?? null,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
const { raw, hash } = generateResetToken();
|
|
122
|
+
await db.passwordResetToken.create({
|
|
123
|
+
data: { userId: user.id, tokenHash: hash, expiresAt },
|
|
124
|
+
});
|
|
125
|
+
const invite = await db.invite.create({
|
|
126
|
+
data: {
|
|
127
|
+
email,
|
|
128
|
+
role,
|
|
129
|
+
status: 'PENDING',
|
|
130
|
+
tokenHash: hash,
|
|
131
|
+
message: input.message?.trim() || null,
|
|
132
|
+
invitedByUserId: config.invitedByUserId ?? null,
|
|
133
|
+
expiresAt,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const inviteUrl = buildAcceptUrl(raw, config);
|
|
137
|
+
const emailed = await sendInviteEmail(email, name, role, inviteUrl, config);
|
|
138
|
+
return {
|
|
139
|
+
success: true,
|
|
140
|
+
emailed,
|
|
141
|
+
inviteUrl: emailed ? undefined : inviteUrl,
|
|
142
|
+
invite: serializeInvite(invite),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* List invites, lazily flipping any past-expiry PENDING rows to EXPIRED so the
|
|
147
|
+
* status is always honest without a background job.
|
|
148
|
+
*/
|
|
149
|
+
export async function listInvites(db, params = {}) {
|
|
150
|
+
await expireStaleInvites(db);
|
|
151
|
+
const where = {};
|
|
152
|
+
if (params.status && params.status !== 'all')
|
|
153
|
+
where.status = params.status.toUpperCase();
|
|
154
|
+
if (params.search)
|
|
155
|
+
where.email = { contains: params.search.trim(), mode: 'insensitive' };
|
|
156
|
+
const rows = await db.invite.findMany({ where, orderBy: { createdAt: 'desc' } });
|
|
157
|
+
return rows.map(serializeInvite);
|
|
158
|
+
}
|
|
159
|
+
/** Flip PENDING invites whose expiry has passed to EXPIRED. Returns the count. */
|
|
160
|
+
export async function expireStaleInvites(db) {
|
|
161
|
+
const res = await db.invite.updateMany({
|
|
162
|
+
where: { status: 'PENDING', expiresAt: { lt: new Date() } },
|
|
163
|
+
data: { status: 'EXPIRED' },
|
|
164
|
+
});
|
|
165
|
+
return res.count ?? 0;
|
|
166
|
+
}
|
|
167
|
+
/** Resend (or revive an expired) invite: fresh token, reset expiry, re-email. */
|
|
168
|
+
export async function resendInvite(db, inviteId, config) {
|
|
169
|
+
const invite = await db.invite.findUnique({ where: { id: inviteId } });
|
|
170
|
+
if (!invite)
|
|
171
|
+
return { success: false, error: 'Invite not found.', code: 'NOT_FOUND' };
|
|
172
|
+
if (invite.status === 'ACCEPTED') {
|
|
173
|
+
return { success: false, error: 'This invitation was already accepted.' };
|
|
174
|
+
}
|
|
175
|
+
if (invite.status === 'CANCELLED') {
|
|
176
|
+
return { success: false, error: 'This invitation was cancelled.' };
|
|
177
|
+
}
|
|
178
|
+
const user = await db.user.findFirst({
|
|
179
|
+
where: { email: { equals: invite.email, mode: 'insensitive' } },
|
|
180
|
+
});
|
|
181
|
+
const expiresAt = new Date(Date.now() + INVITE_EXPIRY_MS);
|
|
182
|
+
const { raw, hash } = generateResetToken();
|
|
183
|
+
if (user) {
|
|
184
|
+
await db.passwordResetToken.updateMany({
|
|
185
|
+
where: { userId: user.id, usedAt: null },
|
|
186
|
+
data: { usedAt: new Date() },
|
|
187
|
+
});
|
|
188
|
+
await db.passwordResetToken.create({ data: { userId: user.id, tokenHash: hash, expiresAt } });
|
|
189
|
+
}
|
|
190
|
+
const updated = await db.invite.update({
|
|
191
|
+
where: { id: inviteId },
|
|
192
|
+
data: { status: 'PENDING', tokenHash: hash, expiresAt },
|
|
193
|
+
});
|
|
194
|
+
const inviteUrl = buildAcceptUrl(raw, config);
|
|
195
|
+
const emailed = await sendInviteEmail(invite.email, user?.name ?? invite.email, invite.role, inviteUrl, config);
|
|
196
|
+
return {
|
|
197
|
+
success: true,
|
|
198
|
+
emailed,
|
|
199
|
+
inviteUrl: emailed ? undefined : inviteUrl,
|
|
200
|
+
invite: serializeInvite(updated),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Cancel an invite and deactivate its placeholder user so the link is dead. */
|
|
204
|
+
export async function cancelInvite(db, inviteId) {
|
|
205
|
+
const invite = await db.invite.findUnique({ where: { id: inviteId } });
|
|
206
|
+
if (!invite)
|
|
207
|
+
return { success: false, error: 'Invite not found.', code: 'NOT_FOUND' };
|
|
208
|
+
if (invite.status === 'ACCEPTED') {
|
|
209
|
+
return { success: false, error: 'This invitation was already accepted.' };
|
|
210
|
+
}
|
|
211
|
+
const user = await db.user.findFirst({
|
|
212
|
+
where: { email: { equals: invite.email, mode: 'insensitive' }, acceptedInviteAt: null },
|
|
213
|
+
});
|
|
214
|
+
if (user) {
|
|
215
|
+
await db.passwordResetToken.updateMany({
|
|
216
|
+
where: { userId: user.id, usedAt: null },
|
|
217
|
+
data: { usedAt: new Date() },
|
|
218
|
+
});
|
|
219
|
+
await db.user.update({ where: { id: user.id }, data: { isActive: false } });
|
|
220
|
+
}
|
|
221
|
+
const updated = await db.invite.update({
|
|
222
|
+
where: { id: inviteId },
|
|
223
|
+
data: { status: 'CANCELLED', cancelledAt: new Date() },
|
|
224
|
+
});
|
|
225
|
+
return { success: true, invite: serializeInvite(updated) };
|
|
226
|
+
}
|
|
227
|
+
/** Change the role on a pending invite (and its placeholder user). */
|
|
228
|
+
export async function updateInviteRole(db, inviteId, role) {
|
|
229
|
+
const normalized = normalizeAssignableRole(role);
|
|
230
|
+
if (!normalized)
|
|
231
|
+
return { success: false, error: 'Select a valid role.', code: 'INVALID_ROLE' };
|
|
232
|
+
const invite = await db.invite.findUnique({ where: { id: inviteId } });
|
|
233
|
+
if (!invite)
|
|
234
|
+
return { success: false, error: 'Invite not found.', code: 'NOT_FOUND' };
|
|
235
|
+
if (invite.status !== 'PENDING') {
|
|
236
|
+
return { success: false, error: 'Only pending invitations can be changed.' };
|
|
237
|
+
}
|
|
238
|
+
await db.user.updateMany({
|
|
239
|
+
where: { email: { equals: invite.email, mode: 'insensitive' }, acceptedInviteAt: null },
|
|
240
|
+
data: { role: normalized },
|
|
241
|
+
});
|
|
242
|
+
const updated = await db.invite.update({ where: { id: inviteId }, data: { role: normalized } });
|
|
243
|
+
return { success: true, invite: serializeInvite(updated) };
|
|
244
|
+
}
|
|
245
|
+
/** Extend a pending invite's expiry and re-sync its reset token expiry. */
|
|
246
|
+
export async function extendInvite(db, inviteId, days) {
|
|
247
|
+
const invite = await db.invite.findUnique({ where: { id: inviteId } });
|
|
248
|
+
if (!invite)
|
|
249
|
+
return { success: false, error: 'Invite not found.', code: 'NOT_FOUND' };
|
|
250
|
+
if (invite.status !== 'PENDING') {
|
|
251
|
+
return { success: false, error: 'Only pending invitations can be extended.' };
|
|
252
|
+
}
|
|
253
|
+
const expiresAt = new Date(Date.now() + expiryDays(days) * 24 * 60 * 60 * 1000);
|
|
254
|
+
const user = await db.user.findFirst({
|
|
255
|
+
where: { email: { equals: invite.email, mode: 'insensitive' }, acceptedInviteAt: null },
|
|
256
|
+
});
|
|
257
|
+
if (user) {
|
|
258
|
+
await db.passwordResetToken.updateMany({
|
|
259
|
+
where: { userId: user.id, usedAt: null },
|
|
260
|
+
data: { expiresAt },
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const updated = await db.invite.update({ where: { id: inviteId }, data: { expiresAt } });
|
|
264
|
+
return { success: true, invite: serializeInvite(updated) };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Mark the pending invite for an email as ACCEPTED. Called when a user sets
|
|
268
|
+
* their password through the reset/accept flow. Safe to call for non-invited
|
|
269
|
+
* users — it simply updates zero rows.
|
|
270
|
+
*/
|
|
271
|
+
export async function markInviteAcceptedForEmail(db, email, userId) {
|
|
272
|
+
const normalized = email.toLowerCase().trim();
|
|
273
|
+
const res = await db.invite.updateMany({
|
|
274
|
+
where: { email: { equals: normalized, mode: 'insensitive' }, status: 'PENDING' },
|
|
275
|
+
data: { status: 'ACCEPTED', acceptedAt: new Date(), acceptedUserId: userId },
|
|
276
|
+
});
|
|
277
|
+
return (res.count ?? 0) > 0;
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=invites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invites.js","sourceRoot":"","sources":["../../src/auth/invites.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAA;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,uBAAuB,EAAE,aAAa,EAAqB,MAAM,YAAY,CAAA;AAEtF,yEAAyE;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AACvD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAU,CAAA;AAErD,MAAM,QAAQ,GAAG,4BAA4B,CAAA;AA0C7C,SAAS,UAAU,CAAC,KAAc;IAChC,MAAM,CAAC,GAAG,KAAK,IAAI,CAAC,CAAA;IACpB,OAAQ,iBAAuC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACrE,CAAC;AAED,SAAS,cAAc,CAAC,QAAgB,EAAE,MAAoB;IAC5D,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IAC9C,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IACnE,OAAO,GAAG,IAAI,GAAG,SAAS,yBAAyB,QAAQ,EAAE,CAAA;AAC/D,CAAC;AAED,SAAS,eAAe,CAAC,GAAQ;IAC/B,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,QAAQ,EAAE,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;QACjC,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,eAAe,EAAE,GAAG,CAAC,eAAe,IAAI,IAAI;QAC5C,SAAS,EAAE,GAAG,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS;QACtF,SAAS,EAAE,GAAG,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS;QACtF,OAAO,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE;KACxD,CAAA;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAC5B,KAAa,EACb,IAAY,EACZ,IAAkB,EAClB,SAAiB,EACjB,MAAoB;IAEpB,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAA;IACnC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACzB,MAAM,GAAG,GAAG,2BAA2B,CAAC;QACtC,SAAS;QACT,QAAQ,EAAE,IAAI;QACd,IAAI;QACJ,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC,CAAA;IACF,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAA;QACtF,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;QAC5E,uEAAuE;QACvE,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,EAAO,EACP,KAAwB,EACxB,MAAoB;IAEpB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAA;IAC9C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,8BAA8B,EAAE,IAAI,EAAE,eAAe,EAAE,CAAA;IACzF,CAAC;IAED,MAAM,IAAI,GAAG,uBAAuB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAChD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,cAAc,EAAE,CAAA;IAChF,CAAC;IAED,kEAAkE;IAClE,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC;QACxC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE;KAC5E,CAAC,CAAA;IACF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,kEAAkE;YACzE,IAAI,EAAE,gBAAgB;YACtB,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC;SACjC,CAAA;IACH,CAAC;IAED,0DAA0D;IAC1D,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;QAC3C,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;KACzD,CAAC,CAAA;IACF,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,wCAAwC;YAC/C,IAAI,EAAE,gBAAgB;SACvB,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAA;IACzC,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAA;IACxE,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IAE9F,mEAAmE;IACnE,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC;QAChC,IAAI,EAAE;YACJ,KAAK;YACL,IAAI;YACJ,IAAI;YACJ,YAAY;YACZ,QAAQ,EAAE,IAAI;YACd,UAAU,EAAE,KAAK;YACjB,aAAa,EAAE,KAAK;YACpB,WAAW,EAAE,MAAM,CAAC,eAAe,IAAI,IAAI;SAC5C;KACF,CAAC,CAAA;IAEF,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,kBAAkB,EAAE,CAAA;IAC1C,MAAM,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC;QACjC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;KACtD,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;QACpC,IAAI,EAAE;YACJ,KAAK;YACL,IAAI;YACJ,MAAM,EAAE,SAAS;YACjB,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI;YACtC,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,IAAI;YAC/C,SAAS;SACV;KACF,CAAC,CAAA;IAEF,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IAC7C,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,CAAA;IAE3E,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO;QACP,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;QAC1C,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC;KAChC,CAAA;AACH,CAAC;AAOD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,EAAO,EACP,SAA4B,EAAE;IAE9B,MAAM,kBAAkB,CAAC,EAAE,CAAC,CAAA;IAE5B,MAAM,KAAK,GAA4B,EAAE,CAAA;IACzC,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,KAAK;QAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAA;IACxF,IAAI,MAAM,CAAC,MAAM;QAAE,KAAK,CAAC,KAAK,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;IAExF,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,CAAA;IAChF,OAAO,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;AAClC,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,EAAO;IAC9C,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC;QACrC,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,EAAE,EAAE;QAC3D,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE;KAC5B,CAAC,CAAA;IACF,OAAO,GAAG,CAAC,KAAK,IAAI,CAAC,CAAA;AACvB,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,EAAO,EACP,QAAgB,EAChB,MAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;IACtE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IACrF,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,uCAAuC,EAAE,CAAA;IAC3E,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAA;IACpE,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;QACnC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;KAChE,CAAC,CAAA;IAEF,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,gBAAgB,CAAC,CAAA;IACzD,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,kBAAkB,EAAE,CAAA;IAE1C,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,EAAE,CAAC,kBAAkB,CAAC,UAAU,CAAC;YACrC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;YACxC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,EAAE;SAC7B,CAAC,CAAA;QACF,MAAM,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;IAC/F,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;QACrC,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE;QACvB,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;KACxD,CAAC,CAAA;IAEF,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IAC7C,MAAM,OAAO,GAAG,MAAM,eAAe,CACnC,MAAM,CAAC,KAAK,EACZ,IAAI,EAAE,IAAI,IAAI,MAAM,CAAC,KAAK,EAC1B,MAAM,CAAC,IAAI,EACX,SAAS,EACT,MAAM,CACP,CAAA;IAED,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO;QACP,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;QAC1C,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC;KACjC,CAAA;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EAAO,EAAE,QAAgB;IAC1D,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;IACtE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IACrF,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,uCAAuC,EAAE,CAAA;IAC3E,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;QACnC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE;KACxF,CAAC,CAAA;IACF,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,EAAE,CAAC,kBAAkB,CAAC,UAAU,CAAC;YACrC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;YACxC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,EAAE;SAC7B,CAAC,CAAA;QACF,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;IAC7E,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;QACrC,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE;QACvB,IAAI,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,EAAE;KACvD,CAAC,CAAA;IACF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAA;AAC5D,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,EAAO,EACP,QAAgB,EAChB,IAAY;IAEZ,MAAM,UAAU,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAA;IAChD,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,cAAc,EAAE,CAAA;IAE/F,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;IACtE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IACrF,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,0CAA0C,EAAE,CAAA;IAC9E,CAAC;IAED,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC;QACvB,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE;QACvF,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;KAC3B,CAAC,CAAA;IACF,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;IAC/F,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAA;AAC5D,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EAAO,EAAE,QAAgB,EAAE,IAAY;IACxE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;IACtE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IACrF,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,2CAA2C,EAAE,CAAA;IAC/E,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IAC/E,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;QACnC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE;KACxF,CAAC,CAAA;IACF,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,EAAE,CAAC,kBAAkB,CAAC,UAAU,CAAC;YACrC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;YACxC,IAAI,EAAE,EAAE,SAAS,EAAE;SACpB,CAAC,CAAA;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;IACxF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAA;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,EAAO,EACP,KAAa,EACb,MAAc;IAEd,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAA;IAC7C,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC;QACrC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE;QAChF,IAAI,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE;KAC7E,CAAC,CAAA;IACF,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;AAC7B,CAAC"}
|
package/dist/auth/reset.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export declare function hashToken(raw: string): string;
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function createPasswordReset(db: any, email: string, config: {
|
|
15
15
|
siteUrl: string;
|
|
16
|
+
/**
|
|
17
|
+
* Admin mount path (e.g. `/admin`, `/securelogin`). The reset link points at
|
|
18
|
+
* `<siteUrl><adminPath>/reset-password`, so this must match where the admin
|
|
19
|
+
* is actually served or the link 404s. Defaults to `/admin`.
|
|
20
|
+
*/
|
|
21
|
+
adminPath?: string;
|
|
16
22
|
platform?: {
|
|
17
23
|
email?: {
|
|
18
24
|
send: (opts: {
|
package/dist/auth/reset.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reset.d.ts","sourceRoot":"","sources":["../../src/auth/reset.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reset.d.ts","sourceRoot":"","sources":["../../src/auth/reset.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;CACb;AAID,4EAA4E;AAC5E,wBAAgB,kBAAkB,IAAI,UAAU,CAI/C;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,GAAG,EACP,KAAK,EAAE,MAAM,EACb,MAAM,EAAE;IACN,OAAO,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE;YACN,IAAI,EAAE,CAAC,IAAI,EAAE;gBAAE,EAAE,EAAE,MAAM,CAAC;gBAAC,OAAO,EAAE,MAAM,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC;gBAAC,IAAI,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;SAC5F,CAAA;KACF,CAAA;CACF,GACA,OAAO,CAAC,IAAI,CAAC,CA4Cf;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,GAAG,EACP,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiF/C"}
|
package/dist/auth/reset.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes, createHash } from 'node:crypto';
|
|
2
2
|
import { hashPassword, validatePasswordPolicy } from './password.js';
|
|
3
3
|
import { getActuateConfig } from '../config/runtime.js';
|
|
4
|
+
import { resolveEmailSender } from '../email/sender.js';
|
|
4
5
|
const TOKEN_EXPIRY_MS = 60 * 60 * 1000; // 1 hour
|
|
5
6
|
/** Generate a cryptographically random reset token and its SHA-256 hash. */
|
|
6
7
|
export function generateResetToken() {
|
|
@@ -41,11 +42,18 @@ export async function createPasswordReset(db, email, config) {
|
|
|
41
42
|
expiresAt: new Date(Date.now() + TOKEN_EXPIRY_MS),
|
|
42
43
|
},
|
|
43
44
|
});
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// Prefer an explicit platform adapter passed by the caller, otherwise fall
|
|
46
|
+
// back to a sender registered by the email plugin. Without this fallback,
|
|
47
|
+
// apps that configure email purely through `emailPlugin()` (the common case)
|
|
48
|
+
// would silently never send reset mail.
|
|
49
|
+
const sender = config.platform?.email ?? resolveEmailSender();
|
|
50
|
+
if (sender) {
|
|
51
|
+
const base = config.siteUrl.replace(/\/$/, '');
|
|
52
|
+
const adminPath = (config.adminPath ?? '/admin').replace(/\/$/, '');
|
|
53
|
+
const resetUrl = `${base}${adminPath}/reset-password?token=${raw}`;
|
|
46
54
|
const { passwordResetTemplate } = await import('./reset-email.js');
|
|
47
55
|
const tpl = passwordResetTemplate({ resetUrl, userName: user.name });
|
|
48
|
-
await
|
|
56
|
+
await sender.send({
|
|
49
57
|
to: user.email,
|
|
50
58
|
subject: tpl.subject,
|
|
51
59
|
html: tpl.html,
|
|
@@ -89,11 +97,33 @@ export async function executePasswordReset(db, rawToken, newPassword) {
|
|
|
89
97
|
return { success: false, error: policy.errors[0] ?? 'Password does not meet requirements.' };
|
|
90
98
|
}
|
|
91
99
|
const passwordHash = await hashPassword(newPassword);
|
|
100
|
+
// If this account has a pending invite, completing the reset *is* the
|
|
101
|
+
// acceptance: mark the invite ACCEPTED and stamp the user as accepted +
|
|
102
|
+
// approved. Done in the same transaction so password + acceptance commit
|
|
103
|
+
// atomically. A normal (non-invite) reset matches no invite and is unaffected.
|
|
104
|
+
let pendingInvite = null;
|
|
105
|
+
try {
|
|
106
|
+
pendingInvite =
|
|
107
|
+
(await db.invite?.findFirst?.({
|
|
108
|
+
where: { email: { equals: resetToken.user.email, mode: 'insensitive' }, status: 'PENDING' },
|
|
109
|
+
})) ?? null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
pendingInvite = null;
|
|
113
|
+
}
|
|
114
|
+
const userData = { passwordHash };
|
|
115
|
+
const ops = [];
|
|
116
|
+
if (pendingInvite) {
|
|
117
|
+
userData.acceptedInviteAt = new Date();
|
|
118
|
+
userData.isApproved = true;
|
|
119
|
+
userData.emailVerified = true;
|
|
120
|
+
ops.push(db.invite.update({
|
|
121
|
+
where: { id: pendingInvite.id },
|
|
122
|
+
data: { status: 'ACCEPTED', acceptedAt: new Date(), acceptedUserId: resetToken.userId },
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
92
125
|
await db.$transaction([
|
|
93
|
-
db.user.update({
|
|
94
|
-
where: { id: resetToken.userId },
|
|
95
|
-
data: { passwordHash },
|
|
96
|
-
}),
|
|
126
|
+
db.user.update({ where: { id: resetToken.userId }, data: userData }),
|
|
97
127
|
db.passwordResetToken.update({
|
|
98
128
|
where: { id: resetToken.id },
|
|
99
129
|
data: { usedAt: new Date() },
|
|
@@ -102,6 +132,7 @@ export async function executePasswordReset(db, rawToken, newPassword) {
|
|
|
102
132
|
where: { userId: resetToken.userId, revokedAt: null },
|
|
103
133
|
data: { revokedAt: new Date() },
|
|
104
134
|
}),
|
|
135
|
+
...ops,
|
|
105
136
|
]);
|
|
106
137
|
return { success: true };
|
|
107
138
|
}
|
package/dist/auth/reset.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reset.js","sourceRoot":"","sources":["../../src/auth/reset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAOvD,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,SAAS;AAEhD,4EAA4E;AAC5E,MAAM,UAAU,kBAAkB;IAChC,MAAM,GAAG,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC3C,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC3D,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;AACtB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,EAAO,EACP,KAAa,EACb,
|
|
1
|
+
{"version":3,"file":"reset.js","sourceRoot":"","sources":["../../src/auth/reset.ts"],"names":[],"mappings":"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"}
|