@rovela-ai/sdk 0.2.0 → 0.3.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/admin/api/accept-invite.d.ts +65 -0
- package/dist/admin/api/accept-invite.d.ts.map +1 -0
- package/dist/admin/api/accept-invite.js +115 -0
- package/dist/admin/api/accept-invite.js.map +1 -0
- package/dist/admin/api/categories.d.ts.map +1 -1
- package/dist/admin/api/categories.js +21 -28
- package/dist/admin/api/categories.js.map +1 -1
- package/dist/admin/api/customers.d.ts.map +1 -1
- package/dist/admin/api/customers.js +17 -25
- package/dist/admin/api/customers.js.map +1 -1
- package/dist/admin/api/forgot-password.d.ts +39 -0
- package/dist/admin/api/forgot-password.d.ts.map +1 -0
- package/dist/admin/api/forgot-password.js +66 -0
- package/dist/admin/api/forgot-password.js.map +1 -0
- package/dist/admin/api/index.d.ts +6 -0
- package/dist/admin/api/index.d.ts.map +1 -1
- package/dist/admin/api/index.js +9 -0
- package/dist/admin/api/index.js.map +1 -1
- package/dist/admin/api/me.d.ts +72 -0
- package/dist/admin/api/me.d.ts.map +1 -0
- package/dist/admin/api/me.js +177 -0
- package/dist/admin/api/me.js.map +1 -0
- package/dist/admin/api/orders.d.ts.map +1 -1
- package/dist/admin/api/orders.js +21 -28
- package/dist/admin/api/orders.js.map +1 -1
- package/dist/admin/api/products.d.ts.map +1 -1
- package/dist/admin/api/products.js +33 -37
- package/dist/admin/api/products.js.map +1 -1
- package/dist/admin/api/refund.d.ts.map +1 -1
- package/dist/admin/api/refund.js +5 -7
- package/dist/admin/api/refund.js.map +1 -1
- package/dist/admin/api/reset-password.d.ts +49 -0
- package/dist/admin/api/reset-password.d.ts.map +1 -0
- package/dist/admin/api/reset-password.js +99 -0
- package/dist/admin/api/reset-password.js.map +1 -0
- package/dist/admin/api/return.d.ts.map +1 -1
- package/dist/admin/api/return.js +9 -12
- package/dist/admin/api/return.js.map +1 -1
- package/dist/admin/api/settings.d.ts.map +1 -1
- package/dist/admin/api/settings.js +9 -12
- package/dist/admin/api/settings.js.map +1 -1
- package/dist/admin/api/shipping.d.ts.map +1 -1
- package/dist/admin/api/shipping.js +65 -61
- package/dist/admin/api/shipping.js.map +1 -1
- package/dist/admin/api/stats.d.ts.map +1 -1
- package/dist/admin/api/stats.js +5 -7
- package/dist/admin/api/stats.js.map +1 -1
- package/dist/admin/api/stripe-status.d.ts.map +1 -1
- package/dist/admin/api/stripe-status.js +5 -7
- package/dist/admin/api/stripe-status.js.map +1 -1
- package/dist/admin/api/tax-zones.d.ts.map +1 -1
- package/dist/admin/api/tax-zones.js +21 -28
- package/dist/admin/api/tax-zones.js.map +1 -1
- package/dist/admin/api/users.d.ts +142 -0
- package/dist/admin/api/users.d.ts.map +1 -0
- package/dist/admin/api/users.js +356 -0
- package/dist/admin/api/users.js.map +1 -0
- package/dist/admin/components/AdminAcceptInviteForm.d.ts +3 -0
- package/dist/admin/components/AdminAcceptInviteForm.d.ts.map +1 -0
- package/dist/admin/components/AdminAcceptInviteForm.js +137 -0
- package/dist/admin/components/AdminAcceptInviteForm.js.map +1 -0
- package/dist/admin/components/AdminAccountPage.d.ts +10 -0
- package/dist/admin/components/AdminAccountPage.d.ts.map +1 -0
- package/dist/admin/components/AdminAccountPage.js +123 -0
- package/dist/admin/components/AdminAccountPage.js.map +1 -0
- package/dist/admin/components/AdminForgotPasswordForm.d.ts +8 -0
- package/dist/admin/components/AdminForgotPasswordForm.d.ts.map +1 -0
- package/dist/admin/components/AdminForgotPasswordForm.js +59 -0
- package/dist/admin/components/AdminForgotPasswordForm.js.map +1 -0
- package/dist/admin/components/AdminNav.d.ts.map +1 -1
- package/dist/admin/components/AdminNav.js +39 -8
- package/dist/admin/components/AdminNav.js.map +1 -1
- package/dist/admin/components/AdminResetPasswordForm.d.ts +12 -0
- package/dist/admin/components/AdminResetPasswordForm.d.ts.map +1 -0
- package/dist/admin/components/AdminResetPasswordForm.js +134 -0
- package/dist/admin/components/AdminResetPasswordForm.js.map +1 -0
- package/dist/admin/components/AdminUserMenu.d.ts.map +1 -1
- package/dist/admin/components/AdminUserMenu.js +2 -2
- package/dist/admin/components/AdminUserMenu.js.map +1 -1
- package/dist/admin/components/InviteUserDialog.d.ts +3 -0
- package/dist/admin/components/InviteUserDialog.d.ts.map +1 -0
- package/dist/admin/components/InviteUserDialog.js +127 -0
- package/dist/admin/components/InviteUserDialog.js.map +1 -0
- package/dist/admin/components/UsersTable.d.ts +3 -0
- package/dist/admin/components/UsersTable.d.ts.map +1 -0
- package/dist/admin/components/UsersTable.js +399 -0
- package/dist/admin/components/UsersTable.js.map +1 -0
- package/dist/admin/components/index.d.ts +9 -0
- package/dist/admin/components/index.d.ts.map +1 -1
- package/dist/admin/components/index.js +9 -0
- package/dist/admin/components/index.js.map +1 -1
- package/dist/admin/config.d.ts.map +1 -1
- package/dist/admin/config.js +23 -1
- package/dist/admin/config.js.map +1 -1
- package/dist/admin/hooks/index.d.ts +4 -0
- package/dist/admin/hooks/index.d.ts.map +1 -1
- package/dist/admin/hooks/index.js +3 -0
- package/dist/admin/hooks/index.js.map +1 -1
- package/dist/admin/hooks/useAdminMe.d.ts +31 -0
- package/dist/admin/hooks/useAdminMe.d.ts.map +1 -0
- package/dist/admin/hooks/useAdminMe.js +103 -0
- package/dist/admin/hooks/useAdminMe.js.map +1 -0
- package/dist/admin/hooks/useAdminPermissions.d.ts +3 -0
- package/dist/admin/hooks/useAdminPermissions.d.ts.map +1 -0
- package/dist/admin/hooks/useAdminPermissions.js +51 -0
- package/dist/admin/hooks/useAdminPermissions.js.map +1 -0
- package/dist/admin/hooks/useAdminUsers.d.ts +3 -0
- package/dist/admin/hooks/useAdminUsers.d.ts.map +1 -0
- package/dist/admin/hooks/useAdminUsers.js +240 -0
- package/dist/admin/hooks/useAdminUsers.js.map +1 -0
- package/dist/admin/index.d.ts +4 -4
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +20 -2
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/permissions.d.ts +92 -0
- package/dist/admin/permissions.d.ts.map +1 -0
- package/dist/admin/permissions.js +201 -0
- package/dist/admin/permissions.js.map +1 -0
- package/dist/admin/server/admin-invite.d.ts +122 -0
- package/dist/admin/server/admin-invite.d.ts.map +1 -0
- package/dist/admin/server/admin-invite.js +235 -0
- package/dist/admin/server/admin-invite.js.map +1 -0
- package/dist/admin/server/admin-password-reset.d.ts +87 -0
- package/dist/admin/server/admin-password-reset.d.ts.map +1 -0
- package/dist/admin/server/admin-password-reset.js +220 -0
- package/dist/admin/server/admin-password-reset.js.map +1 -0
- package/dist/admin/server/admin-self-service.d.ts +86 -0
- package/dist/admin/server/admin-self-service.d.ts.map +1 -0
- package/dist/admin/server/admin-self-service.js +188 -0
- package/dist/admin/server/admin-self-service.js.map +1 -0
- package/dist/admin/server/admin-service.d.ts.map +1 -1
- package/dist/admin/server/admin-service.js +21 -2
- package/dist/admin/server/admin-service.js.map +1 -1
- package/dist/admin/server/admin-session.d.ts +126 -0
- package/dist/admin/server/admin-session.d.ts.map +1 -0
- package/dist/admin/server/admin-session.js +215 -0
- package/dist/admin/server/admin-session.js.map +1 -0
- package/dist/admin/server/index.d.ts +7 -0
- package/dist/admin/server/index.d.ts.map +1 -1
- package/dist/admin/server/index.js +20 -0
- package/dist/admin/server/index.js.map +1 -1
- package/dist/admin/server/user-management.d.ts +223 -0
- package/dist/admin/server/user-management.d.ts.map +1 -0
- package/dist/admin/server/user-management.js +846 -0
- package/dist/admin/server/user-management.js.map +1 -0
- package/dist/admin/types.d.ts +153 -2
- package/dist/admin/types.d.ts.map +1 -1
- package/dist/core/db/queries.d.ts +19 -13
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/schema.d.ts +327 -9
- package/dist/core/db/schema.d.ts.map +1 -1
- package/dist/core/db/schema.js +80 -3
- package/dist/core/db/schema.js.map +1 -1
- package/dist/core/types.d.ts +19 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/emails/index.d.ts +2 -2
- package/dist/emails/index.d.ts.map +1 -1
- package/dist/emails/index.js +3 -1
- package/dist/emails/index.js.map +1 -1
- package/dist/emails/send/admin-auth.d.ts +94 -0
- package/dist/emails/send/admin-auth.d.ts.map +1 -0
- package/dist/emails/send/admin-auth.js +118 -0
- package/dist/emails/send/admin-auth.js.map +1 -0
- package/dist/emails/send/index.d.ts +2 -0
- package/dist/emails/send/index.d.ts.map +1 -1
- package/dist/emails/send/index.js +4 -0
- package/dist/emails/send/index.js.map +1 -1
- package/dist/emails/templates/admin-invite.d.ts +40 -0
- package/dist/emails/templates/admin-invite.d.ts.map +1 -0
- package/dist/emails/templates/admin-invite.js +62 -0
- package/dist/emails/templates/admin-invite.js.map +1 -0
- package/dist/emails/templates/index.d.ts +1 -0
- package/dist/emails/templates/index.d.ts.map +1 -1
- package/dist/emails/templates/index.js +4 -0
- package/dist/emails/templates/index.js.map +1 -1
- package/dist/emails/types.d.ts +22 -1
- package/dist/emails/types.d.ts.map +1 -1
- package/package.json +21 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rovela/sdk/admin/server/admin-password-reset
|
|
3
|
+
*
|
|
4
|
+
* Admin password reset token management.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `auth/server/password-reset-service.ts` (the customer version),
|
|
7
|
+
* with one critical addition: admins whose status is not 'active' are
|
|
8
|
+
* silently skipped. This blocks deactivated and invited admins from
|
|
9
|
+
* recovering access via the reset flow — deactivated admins must stay
|
|
10
|
+
* deactivated, invited admins must go through the invite-accept flow
|
|
11
|
+
* (Phase 3). The caller always receives `{success: true}` regardless
|
|
12
|
+
* to preserve enumeration safety.
|
|
13
|
+
*
|
|
14
|
+
* Each store has its own database (via Neon branches), so there's no
|
|
15
|
+
* tenant filtering. The `admin_password_reset_tokens` table was created
|
|
16
|
+
* for every active store during the Phase 0 migration.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Request a password reset for an admin.
|
|
20
|
+
*
|
|
21
|
+
* Creates a token and sends the reset email. Always returns `{success: true}`
|
|
22
|
+
* regardless of whether the email belongs to a real admin — this prevents
|
|
23
|
+
* email enumeration attacks and keeps the HTTP response shape identical for
|
|
24
|
+
* all outcomes.
|
|
25
|
+
*
|
|
26
|
+
* Silently skipped cases (all return success without side effects):
|
|
27
|
+
* - Email doesn't belong to any admin
|
|
28
|
+
* - Admin exists but status !== 'active' (deactivated or invited)
|
|
29
|
+
* - Email sending fails (error is logged, not exposed)
|
|
30
|
+
*
|
|
31
|
+
* @param email - The admin's email address
|
|
32
|
+
* @returns Always `{success: true}` — see above
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* await requestAdminPasswordReset('owner@store.com')
|
|
37
|
+
* // Always show: "If an account exists with this email, we've sent a reset link."
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function requestAdminPasswordReset(email: string): Promise<{
|
|
41
|
+
success: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Validate a reset token without consuming it.
|
|
45
|
+
*
|
|
46
|
+
* Used by the reset page to decide whether to show the "enter new password"
|
|
47
|
+
* form or an "invalid/expired link" error. Expired tokens are cleaned up
|
|
48
|
+
* opportunistically as a side effect.
|
|
49
|
+
*
|
|
50
|
+
* @param token - The token from the reset link
|
|
51
|
+
*/
|
|
52
|
+
export declare function validateAdminResetToken(token: string): Promise<{
|
|
53
|
+
valid: boolean;
|
|
54
|
+
error?: string;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Reset an admin's password using a valid token.
|
|
58
|
+
*
|
|
59
|
+
* Validates the token, updates the password via `updateAdminPassword`
|
|
60
|
+
* (which handles hashing), then deletes ALL reset tokens for that admin
|
|
61
|
+
* so a stale link can't be reused after a successful reset.
|
|
62
|
+
*
|
|
63
|
+
* @param token - The token from the reset link
|
|
64
|
+
* @param newPassword - The new plain-text password (will be hashed)
|
|
65
|
+
*/
|
|
66
|
+
export declare function resetAdminPassword(token: string, newPassword: string): Promise<{
|
|
67
|
+
success: boolean;
|
|
68
|
+
error?: string;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Delete all password reset tokens for a specific admin.
|
|
72
|
+
*
|
|
73
|
+
* Called at two points:
|
|
74
|
+
* 1. Before issuing a new token (clear stale ones)
|
|
75
|
+
* 2. After a successful reset (invalidate the used batch)
|
|
76
|
+
*/
|
|
77
|
+
export declare function deleteAdminPasswordResetTokens(adminId: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Delete expired reset tokens across all admins.
|
|
80
|
+
*
|
|
81
|
+
* Optional hook for a periodic cleanup job (e.g. cron). Safe to call
|
|
82
|
+
* anytime — no-op if nothing is expired.
|
|
83
|
+
*
|
|
84
|
+
* @returns Number of tokens deleted
|
|
85
|
+
*/
|
|
86
|
+
export declare function cleanupExpiredAdminResetTokens(): Promise<number>;
|
|
87
|
+
//# sourceMappingURL=admin-password-reset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-password-reset.d.ts","sourceRoot":"","sources":["../../../src/admin/server/admin-password-reset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAkCH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACtE,OAAO,EAAE,OAAO,CAAA;CACjB,CAAC,CAoDD;AAMD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACpE,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAC,CA6BD;AAMD;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAkC/C;AAMD;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;;;GAOG;AACH,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,MAAM,CAAC,CAStE"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rovela/sdk/admin/server/admin-password-reset
|
|
3
|
+
*
|
|
4
|
+
* Admin password reset token management.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `auth/server/password-reset-service.ts` (the customer version),
|
|
7
|
+
* with one critical addition: admins whose status is not 'active' are
|
|
8
|
+
* silently skipped. This blocks deactivated and invited admins from
|
|
9
|
+
* recovering access via the reset flow — deactivated admins must stay
|
|
10
|
+
* deactivated, invited admins must go through the invite-accept flow
|
|
11
|
+
* (Phase 3). The caller always receives `{success: true}` regardless
|
|
12
|
+
* to preserve enumeration safety.
|
|
13
|
+
*
|
|
14
|
+
* Each store has its own database (via Neon branches), so there's no
|
|
15
|
+
* tenant filtering. The `admin_password_reset_tokens` table was created
|
|
16
|
+
* for every active store during the Phase 0 migration.
|
|
17
|
+
*/
|
|
18
|
+
import { eq, lt } from 'drizzle-orm';
|
|
19
|
+
import { nanoid } from 'nanoid';
|
|
20
|
+
import { getDb } from '../../core/db/client';
|
|
21
|
+
import * as schema from '../../core/db/schema';
|
|
22
|
+
import { findAdminByEmail, updateAdminPassword } from './admin-service';
|
|
23
|
+
import { sendAdminPasswordResetEmail } from '../../emails/send/admin-auth';
|
|
24
|
+
import { getStoreUrl } from '../../emails/config';
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Constants
|
|
27
|
+
// =============================================================================
|
|
28
|
+
/**
|
|
29
|
+
* Password reset token expiry time in milliseconds (1 hour).
|
|
30
|
+
* Matches customer reset flow.
|
|
31
|
+
*/
|
|
32
|
+
const TOKEN_EXPIRY_MS = 60 * 60 * 1000;
|
|
33
|
+
/**
|
|
34
|
+
* Token length (URL-safe, ~192 bits of entropy with nanoid).
|
|
35
|
+
*/
|
|
36
|
+
const TOKEN_LENGTH = 32;
|
|
37
|
+
/**
|
|
38
|
+
* Display string for the expiry duration — passed to the email template.
|
|
39
|
+
*/
|
|
40
|
+
const TOKEN_EXPIRY_HOURS = '1';
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Request reset
|
|
43
|
+
// =============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Request a password reset for an admin.
|
|
46
|
+
*
|
|
47
|
+
* Creates a token and sends the reset email. Always returns `{success: true}`
|
|
48
|
+
* regardless of whether the email belongs to a real admin — this prevents
|
|
49
|
+
* email enumeration attacks and keeps the HTTP response shape identical for
|
|
50
|
+
* all outcomes.
|
|
51
|
+
*
|
|
52
|
+
* Silently skipped cases (all return success without side effects):
|
|
53
|
+
* - Email doesn't belong to any admin
|
|
54
|
+
* - Admin exists but status !== 'active' (deactivated or invited)
|
|
55
|
+
* - Email sending fails (error is logged, not exposed)
|
|
56
|
+
*
|
|
57
|
+
* @param email - The admin's email address
|
|
58
|
+
* @returns Always `{success: true}` — see above
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* await requestAdminPasswordReset('owner@store.com')
|
|
63
|
+
* // Always show: "If an account exists with this email, we've sent a reset link."
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export async function requestAdminPasswordReset(email) {
|
|
67
|
+
const admin = await findAdminByEmail(email);
|
|
68
|
+
// Enumeration-safe fast exits
|
|
69
|
+
if (!admin) {
|
|
70
|
+
return { success: true };
|
|
71
|
+
}
|
|
72
|
+
// Block deactivated and invited admins from using the reset flow.
|
|
73
|
+
// Deactivated: must stay locked out. Invited: should use the invite
|
|
74
|
+
// accept flow (Phase 3), not the reset flow.
|
|
75
|
+
const status = admin.status ?? 'active';
|
|
76
|
+
if (status !== 'active') {
|
|
77
|
+
return { success: true };
|
|
78
|
+
}
|
|
79
|
+
// Clear any existing reset tokens for this admin — "only the latest
|
|
80
|
+
// reset link works" policy, same as customer flow.
|
|
81
|
+
await deleteAdminPasswordResetTokens(admin.id);
|
|
82
|
+
// Generate new token
|
|
83
|
+
const db = getDb();
|
|
84
|
+
const token = nanoid(TOKEN_LENGTH);
|
|
85
|
+
const expires = new Date(Date.now() + TOKEN_EXPIRY_MS);
|
|
86
|
+
await db.insert(schema.adminPasswordResetTokens).values({
|
|
87
|
+
adminId: admin.id,
|
|
88
|
+
token,
|
|
89
|
+
expires,
|
|
90
|
+
});
|
|
91
|
+
// Send the email. Failures are logged but never propagated — returning
|
|
92
|
+
// success keeps the enumeration-safe contract and prevents leaking
|
|
93
|
+
// infrastructure state (e.g. "Resend is down").
|
|
94
|
+
try {
|
|
95
|
+
const storeUrl = getStoreUrl();
|
|
96
|
+
const resetLink = `${storeUrl}/admin/reset-password?token=${encodeURIComponent(token)}`;
|
|
97
|
+
await sendAdminPasswordResetEmail({
|
|
98
|
+
to: email,
|
|
99
|
+
adminName: admin.name,
|
|
100
|
+
resetLink,
|
|
101
|
+
expiryTime: TOKEN_EXPIRY_HOURS,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error('[admin-password-reset] Failed to send reset email:', error instanceof Error ? error.message : error);
|
|
106
|
+
// Do not fail the request — enumeration safety + infra privacy.
|
|
107
|
+
}
|
|
108
|
+
return { success: true };
|
|
109
|
+
}
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// Validate token (non-destructive check)
|
|
112
|
+
// =============================================================================
|
|
113
|
+
/**
|
|
114
|
+
* Validate a reset token without consuming it.
|
|
115
|
+
*
|
|
116
|
+
* Used by the reset page to decide whether to show the "enter new password"
|
|
117
|
+
* form or an "invalid/expired link" error. Expired tokens are cleaned up
|
|
118
|
+
* opportunistically as a side effect.
|
|
119
|
+
*
|
|
120
|
+
* @param token - The token from the reset link
|
|
121
|
+
*/
|
|
122
|
+
export async function validateAdminResetToken(token) {
|
|
123
|
+
const db = getDb();
|
|
124
|
+
const [record] = await db
|
|
125
|
+
.select()
|
|
126
|
+
.from(schema.adminPasswordResetTokens)
|
|
127
|
+
.where(eq(schema.adminPasswordResetTokens.token, token))
|
|
128
|
+
.limit(1);
|
|
129
|
+
if (!record) {
|
|
130
|
+
return {
|
|
131
|
+
valid: false,
|
|
132
|
+
error: 'Invalid or expired reset link. Please request a new one.',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (new Date() > record.expires) {
|
|
136
|
+
// Opportunistic cleanup
|
|
137
|
+
await db
|
|
138
|
+
.delete(schema.adminPasswordResetTokens)
|
|
139
|
+
.where(eq(schema.adminPasswordResetTokens.id, record.id));
|
|
140
|
+
return {
|
|
141
|
+
valid: false,
|
|
142
|
+
error: 'Reset link has expired. Please request a new one.',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return { valid: true };
|
|
146
|
+
}
|
|
147
|
+
// =============================================================================
|
|
148
|
+
// Reset password (consumes token)
|
|
149
|
+
// =============================================================================
|
|
150
|
+
/**
|
|
151
|
+
* Reset an admin's password using a valid token.
|
|
152
|
+
*
|
|
153
|
+
* Validates the token, updates the password via `updateAdminPassword`
|
|
154
|
+
* (which handles hashing), then deletes ALL reset tokens for that admin
|
|
155
|
+
* so a stale link can't be reused after a successful reset.
|
|
156
|
+
*
|
|
157
|
+
* @param token - The token from the reset link
|
|
158
|
+
* @param newPassword - The new plain-text password (will be hashed)
|
|
159
|
+
*/
|
|
160
|
+
export async function resetAdminPassword(token, newPassword) {
|
|
161
|
+
const db = getDb();
|
|
162
|
+
const [record] = await db
|
|
163
|
+
.select()
|
|
164
|
+
.from(schema.adminPasswordResetTokens)
|
|
165
|
+
.where(eq(schema.adminPasswordResetTokens.token, token))
|
|
166
|
+
.limit(1);
|
|
167
|
+
if (!record) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: 'Invalid or expired reset link. Please request a new one.',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (new Date() > record.expires) {
|
|
174
|
+
await db
|
|
175
|
+
.delete(schema.adminPasswordResetTokens)
|
|
176
|
+
.where(eq(schema.adminPasswordResetTokens.id, record.id));
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
error: 'Reset link has expired. Please request a new one.',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Hash + update via the existing admin-service helper
|
|
183
|
+
await updateAdminPassword(record.adminId, newPassword);
|
|
184
|
+
// Wipe all reset tokens for this admin — one-time use for the whole batch
|
|
185
|
+
await deleteAdminPasswordResetTokens(record.adminId);
|
|
186
|
+
return { success: true };
|
|
187
|
+
}
|
|
188
|
+
// =============================================================================
|
|
189
|
+
// Housekeeping
|
|
190
|
+
// =============================================================================
|
|
191
|
+
/**
|
|
192
|
+
* Delete all password reset tokens for a specific admin.
|
|
193
|
+
*
|
|
194
|
+
* Called at two points:
|
|
195
|
+
* 1. Before issuing a new token (clear stale ones)
|
|
196
|
+
* 2. After a successful reset (invalidate the used batch)
|
|
197
|
+
*/
|
|
198
|
+
export async function deleteAdminPasswordResetTokens(adminId) {
|
|
199
|
+
const db = getDb();
|
|
200
|
+
await db
|
|
201
|
+
.delete(schema.adminPasswordResetTokens)
|
|
202
|
+
.where(eq(schema.adminPasswordResetTokens.adminId, adminId));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Delete expired reset tokens across all admins.
|
|
206
|
+
*
|
|
207
|
+
* Optional hook for a periodic cleanup job (e.g. cron). Safe to call
|
|
208
|
+
* anytime — no-op if nothing is expired.
|
|
209
|
+
*
|
|
210
|
+
* @returns Number of tokens deleted
|
|
211
|
+
*/
|
|
212
|
+
export async function cleanupExpiredAdminResetTokens() {
|
|
213
|
+
const db = getDb();
|
|
214
|
+
const result = await db
|
|
215
|
+
.delete(schema.adminPasswordResetTokens)
|
|
216
|
+
.where(lt(schema.adminPasswordResetTokens.expires, new Date()))
|
|
217
|
+
.returning({ id: schema.adminPasswordResetTokens.id });
|
|
218
|
+
return result.length;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=admin-password-reset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-password-reset.js","sourceRoot":"","sources":["../../../src/admin/server/admin-password-reset.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rovela/sdk/admin/server/admin-self-service
|
|
3
|
+
*
|
|
4
|
+
* Self-service helpers for logged-in admins — change their own password,
|
|
5
|
+
* edit their own profile. Never accepts an `actor` vs `target` distinction:
|
|
6
|
+
* these operations always act on the caller themselves, enforced by the
|
|
7
|
+
* API layer passing `session.user.id` as the adminId.
|
|
8
|
+
*
|
|
9
|
+
* # What these do that admin-service.ts doesn't
|
|
10
|
+
*
|
|
11
|
+
* `admin-service.ts` has the raw CRUD helpers (`updateAdmin`,
|
|
12
|
+
* `updateAdminPassword`). Those are called from multiple contexts —
|
|
13
|
+
* forgot-password flow, invite acceptance, emergency reset, user-management
|
|
14
|
+
* actions. This file wraps them in self-service semantics:
|
|
15
|
+
*
|
|
16
|
+
* 1. `changeOwnPassword` requires the current password as proof of
|
|
17
|
+
* identity. Mere session possession isn't enough — we want defense
|
|
18
|
+
* against "attacker on coffee shop laptop" scenarios where the
|
|
19
|
+
* session cookie is borrowed.
|
|
20
|
+
*
|
|
21
|
+
* 2. `updateOwnProfile` checks email uniqueness against every other
|
|
22
|
+
* admin (not just-not-self) before persisting, to surface clean
|
|
23
|
+
* error codes to the caller.
|
|
24
|
+
*
|
|
25
|
+
* Both helpers return typed discriminated unions — they never throw on
|
|
26
|
+
* business errors, only on unexpected infra failures (DB connectivity,
|
|
27
|
+
* which the API layer catches).
|
|
28
|
+
*/
|
|
29
|
+
import type { StoreAdmin } from '../../core/db/schema';
|
|
30
|
+
export interface SelfServiceError {
|
|
31
|
+
code: 'NOT_FOUND' | 'INVALID_CREDENTIALS' | 'VALIDATION_ERROR' | 'EMAIL_EXISTS';
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
export type ChangeOwnPasswordResult = {
|
|
35
|
+
ok: true;
|
|
36
|
+
} | {
|
|
37
|
+
ok: false;
|
|
38
|
+
error: SelfServiceError;
|
|
39
|
+
};
|
|
40
|
+
export type UpdateOwnProfileResult = {
|
|
41
|
+
ok: true;
|
|
42
|
+
admin: StoreAdmin;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
error: SelfServiceError;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Change the logged-in admin's password.
|
|
49
|
+
*
|
|
50
|
+
* Requires the current password as proof of identity. Validates the new
|
|
51
|
+
* password via the shared `validatePassword` helper (min 8 chars). On
|
|
52
|
+
* success, bumps `session_version` (via `updateAdminPassword`), cleans up
|
|
53
|
+
* any stale reset tokens, and invalidates the in-memory session cache so
|
|
54
|
+
* other active sessions for this admin are kicked out on their next
|
|
55
|
+
* request.
|
|
56
|
+
*
|
|
57
|
+
* The admin's CURRENT session (the one that initiated this change) also
|
|
58
|
+
* carries a now-stale `sessionVersion` JWT claim, which means the next
|
|
59
|
+
* request from that session will also fail `requireAdmin`'s version
|
|
60
|
+
* check → forced logout. That's correct behavior: after a password
|
|
61
|
+
* rotation, the user must re-authenticate with the new password.
|
|
62
|
+
*
|
|
63
|
+
* If that's undesirable (the UI would need to immediately re-sign-in),
|
|
64
|
+
* the API handler can call `nextAuthSignIn` server-side after a
|
|
65
|
+
* successful change. For Phase 4, we accept the forced re-login as the
|
|
66
|
+
* honest behavior.
|
|
67
|
+
*/
|
|
68
|
+
export declare function changeOwnPassword(adminId: string, currentPassword: string, newPassword: string): Promise<ChangeOwnPasswordResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Update the logged-in admin's name and/or email.
|
|
71
|
+
*
|
|
72
|
+
* Email uniqueness is checked against every OTHER admin (not self). If
|
|
73
|
+
* both fields are omitted, returns a VALIDATION_ERROR so the caller gets
|
|
74
|
+
* a clean error rather than a silent no-op.
|
|
75
|
+
*
|
|
76
|
+
* Changing email does NOT require a separate re-verification — the admin
|
|
77
|
+
* is already authenticated and the client-side confirmation dialog in
|
|
78
|
+
* `AdminAccountPage` warns them about the forgot-password implication.
|
|
79
|
+
*
|
|
80
|
+
* No sessionVersion bump — profile changes don't invalidate sessions.
|
|
81
|
+
*/
|
|
82
|
+
export declare function updateOwnProfile(adminId: string, data: {
|
|
83
|
+
name?: string;
|
|
84
|
+
email?: string;
|
|
85
|
+
}): Promise<UpdateOwnProfileResult>;
|
|
86
|
+
//# sourceMappingURL=admin-self-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-self-service.d.ts","sourceRoot":"","sources":["../../../src/admin/server/admin-self-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAMtD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EACA,WAAW,GACX,qBAAqB,GACrB,kBAAkB,GAClB,cAAc,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,MAAM,uBAAuB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,CAAA;AAE1C,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,CAAA;AAM1C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,uBAAuB,CAAC,CA0DlC;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC,OAAO,CAAC,sBAAsB,CAAC,CAuEjC"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rovela/sdk/admin/server/admin-self-service
|
|
3
|
+
*
|
|
4
|
+
* Self-service helpers for logged-in admins — change their own password,
|
|
5
|
+
* edit their own profile. Never accepts an `actor` vs `target` distinction:
|
|
6
|
+
* these operations always act on the caller themselves, enforced by the
|
|
7
|
+
* API layer passing `session.user.id` as the adminId.
|
|
8
|
+
*
|
|
9
|
+
* # What these do that admin-service.ts doesn't
|
|
10
|
+
*
|
|
11
|
+
* `admin-service.ts` has the raw CRUD helpers (`updateAdmin`,
|
|
12
|
+
* `updateAdminPassword`). Those are called from multiple contexts —
|
|
13
|
+
* forgot-password flow, invite acceptance, emergency reset, user-management
|
|
14
|
+
* actions. This file wraps them in self-service semantics:
|
|
15
|
+
*
|
|
16
|
+
* 1. `changeOwnPassword` requires the current password as proof of
|
|
17
|
+
* identity. Mere session possession isn't enough — we want defense
|
|
18
|
+
* against "attacker on coffee shop laptop" scenarios where the
|
|
19
|
+
* session cookie is borrowed.
|
|
20
|
+
*
|
|
21
|
+
* 2. `updateOwnProfile` checks email uniqueness against every other
|
|
22
|
+
* admin (not just-not-self) before persisting, to surface clean
|
|
23
|
+
* error codes to the caller.
|
|
24
|
+
*
|
|
25
|
+
* Both helpers return typed discriminated unions — they never throw on
|
|
26
|
+
* business errors, only on unexpected infra failures (DB connectivity,
|
|
27
|
+
* which the API layer catches).
|
|
28
|
+
*/
|
|
29
|
+
import { eq, and, ne } from 'drizzle-orm';
|
|
30
|
+
import { getDb } from '../../core/db/client';
|
|
31
|
+
import * as schema from '../../core/db/schema';
|
|
32
|
+
import { verifyPassword, validatePassword } from '../../auth/server/password';
|
|
33
|
+
import { findAdminById, updateAdminPassword, updateAdmin } from './admin-service';
|
|
34
|
+
import { deleteAdminPasswordResetTokens } from './admin-password-reset';
|
|
35
|
+
import { invalidateAdminSession } from './admin-session';
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// Change own password
|
|
38
|
+
// =============================================================================
|
|
39
|
+
/**
|
|
40
|
+
* Change the logged-in admin's password.
|
|
41
|
+
*
|
|
42
|
+
* Requires the current password as proof of identity. Validates the new
|
|
43
|
+
* password via the shared `validatePassword` helper (min 8 chars). On
|
|
44
|
+
* success, bumps `session_version` (via `updateAdminPassword`), cleans up
|
|
45
|
+
* any stale reset tokens, and invalidates the in-memory session cache so
|
|
46
|
+
* other active sessions for this admin are kicked out on their next
|
|
47
|
+
* request.
|
|
48
|
+
*
|
|
49
|
+
* The admin's CURRENT session (the one that initiated this change) also
|
|
50
|
+
* carries a now-stale `sessionVersion` JWT claim, which means the next
|
|
51
|
+
* request from that session will also fail `requireAdmin`'s version
|
|
52
|
+
* check → forced logout. That's correct behavior: after a password
|
|
53
|
+
* rotation, the user must re-authenticate with the new password.
|
|
54
|
+
*
|
|
55
|
+
* If that's undesirable (the UI would need to immediately re-sign-in),
|
|
56
|
+
* the API handler can call `nextAuthSignIn` server-side after a
|
|
57
|
+
* successful change. For Phase 4, we accept the forced re-login as the
|
|
58
|
+
* honest behavior.
|
|
59
|
+
*/
|
|
60
|
+
export async function changeOwnPassword(adminId, currentPassword, newPassword) {
|
|
61
|
+
// 1. Load the current admin
|
|
62
|
+
const admin = await findAdminById(adminId);
|
|
63
|
+
if (!admin) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: { code: 'NOT_FOUND', message: 'Admin not found.' },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// 2. No password on file → invited admin can't self-service
|
|
70
|
+
if (!admin.passwordHash) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: {
|
|
74
|
+
code: 'INVALID_CREDENTIALS',
|
|
75
|
+
message: 'Current password is incorrect.',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// 3. Verify current password — same generic error message on failure
|
|
80
|
+
// so we never reveal whether the account exists or is in a weird state.
|
|
81
|
+
const isValid = await verifyPassword(currentPassword, admin.passwordHash);
|
|
82
|
+
if (!isValid) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: {
|
|
86
|
+
code: 'INVALID_CREDENTIALS',
|
|
87
|
+
message: 'Current password is incorrect.',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// 4. Validate new password strength
|
|
92
|
+
const check = validatePassword(newPassword);
|
|
93
|
+
if (!check.valid) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: {
|
|
97
|
+
code: 'VALIDATION_ERROR',
|
|
98
|
+
message: check.error || 'Invalid new password.',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// 5. Update (bumps session_version internally)
|
|
103
|
+
await updateAdminPassword(adminId, newPassword);
|
|
104
|
+
// 6. Defensive cleanup — any stale reset tokens shouldn't grant
|
|
105
|
+
// post-rotation access.
|
|
106
|
+
await deleteAdminPasswordResetTokens(adminId);
|
|
107
|
+
// 7. Flush the 30s per-admin status cache so the next request reads
|
|
108
|
+
// the new session_version immediately.
|
|
109
|
+
invalidateAdminSession(adminId);
|
|
110
|
+
return { ok: true };
|
|
111
|
+
}
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// Update own profile
|
|
114
|
+
// =============================================================================
|
|
115
|
+
/**
|
|
116
|
+
* Update the logged-in admin's name and/or email.
|
|
117
|
+
*
|
|
118
|
+
* Email uniqueness is checked against every OTHER admin (not self). If
|
|
119
|
+
* both fields are omitted, returns a VALIDATION_ERROR so the caller gets
|
|
120
|
+
* a clean error rather than a silent no-op.
|
|
121
|
+
*
|
|
122
|
+
* Changing email does NOT require a separate re-verification — the admin
|
|
123
|
+
* is already authenticated and the client-side confirmation dialog in
|
|
124
|
+
* `AdminAccountPage` warns them about the forgot-password implication.
|
|
125
|
+
*
|
|
126
|
+
* No sessionVersion bump — profile changes don't invalidate sessions.
|
|
127
|
+
*/
|
|
128
|
+
export async function updateOwnProfile(adminId, data) {
|
|
129
|
+
const name = typeof data.name === 'string' ? data.name.trim() : undefined;
|
|
130
|
+
const email = typeof data.email === 'string' ? data.email.trim().toLowerCase() : undefined;
|
|
131
|
+
if (name === undefined && email === undefined) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: {
|
|
135
|
+
code: 'VALIDATION_ERROR',
|
|
136
|
+
message: 'Provide at least one field to update (name or email).',
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Validate name length (if provided)
|
|
141
|
+
if (name !== undefined && name.length < 2) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error: {
|
|
145
|
+
code: 'VALIDATION_ERROR',
|
|
146
|
+
message: 'Name must be at least 2 characters.',
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Validate email format (if provided)
|
|
151
|
+
if (email !== undefined && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: {
|
|
155
|
+
code: 'VALIDATION_ERROR',
|
|
156
|
+
message: 'Please enter a valid email address.',
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// Email uniqueness — only if email is being changed
|
|
161
|
+
if (email !== undefined) {
|
|
162
|
+
const db = getDb();
|
|
163
|
+
const [collision] = await db
|
|
164
|
+
.select({ id: schema.storeAdmins.id })
|
|
165
|
+
.from(schema.storeAdmins)
|
|
166
|
+
.where(and(eq(schema.storeAdmins.email, email), ne(schema.storeAdmins.id, adminId)))
|
|
167
|
+
.limit(1);
|
|
168
|
+
if (collision) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: {
|
|
172
|
+
code: 'EMAIL_EXISTS',
|
|
173
|
+
message: 'Another admin already uses this email.',
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Delegate to the shared helper
|
|
179
|
+
const updated = await updateAdmin(adminId, { name, email });
|
|
180
|
+
if (!updated) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
error: { code: 'NOT_FOUND', message: 'Admin not found.' },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return { ok: true, admin: updated };
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=admin-self-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-self-service.js","sourceRoot":"","sources":["../../../src/admin/server/admin-self-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC5C,OAAO,KAAK,MAAM,MAAM,sBAAsB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AACjF,OAAO,EAAE,8BAA8B,EAAE,MAAM,wBAAwB,CAAA;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAwBxD,gFAAgF;AAChF,sBAAsB;AACtB,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAe,EACf,eAAuB,EACvB,WAAmB;IAEnB,4BAA4B;IAC5B,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAA;IAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,kBAAkB,EAAE;SAC1D,CAAA;IACH,CAAC;IAED,4DAA4D;IAC5D,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QACxB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,qBAAqB;gBAC3B,OAAO,EAAE,gCAAgC;aAC1C;SACF,CAAA;IACH,CAAC;IAED,qEAAqE;IACrE,wEAAwE;IACxE,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,eAAe,EAAE,KAAK,CAAC,YAAY,CAAC,CAAA;IACzE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,qBAAqB;gBAC3B,OAAO,EAAE,gCAAgC;aAC1C;SACF,CAAA;IACH,CAAC;IAED,oCAAoC;IACpC,MAAM,KAAK,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAA;IAC3C,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,KAAK,CAAC,KAAK,IAAI,uBAAuB;aAChD;SACF,CAAA;IACH,CAAC;IAED,+CAA+C;IAC/C,MAAM,mBAAmB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;IAE/C,gEAAgE;IAChE,wBAAwB;IACxB,MAAM,8BAA8B,CAAC,OAAO,CAAC,CAAA;IAE7C,oEAAoE;IACpE,uCAAuC;IACvC,sBAAsB,CAAC,OAAO,CAAC,CAAA;IAE/B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;AACrB,CAAC;AAED,gFAAgF;AAChF,qBAAqB;AACrB,gFAAgF;AAEhF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAe,EACf,IAAuC;IAEvC,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IACzE,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IAE1F,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9C,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,uDAAuD;aACjE;SACF,CAAA;IACH,CAAC;IAED,qCAAqC;IACrC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,qCAAqC;aAC/C;SACF,CAAA;IACH,CAAC;IAED,sCAAsC;IACtC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,4BAA4B,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrE,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,qCAAqC;aAC/C;SACF,CAAA;IACH,CAAC;IAED,oDAAoD;IACpD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;QAClB,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,EAAE;aACzB,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC;aACrC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;aACxB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,EACnC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,CACnC,CACF;aACA,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,IAAI,SAAS,EAAE,CAAC;YACd,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE;oBACL,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,wCAAwC;iBAClD;aACF,CAAA;QACH,CAAC;IACH,CAAC;IAED,gCAAgC;IAChC,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC3D,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,kBAAkB,EAAE;SAC1D,CAAA;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;AACrC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-service.d.ts","sourceRoot":"","sources":["../../../src/admin/server/admin-service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,MAAM,MAAM,sBAAsB,CAAA;AAE9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAM5C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC,UAAU,CAAA;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,IAAI,CAAA;IACb,KAAK,EAAE,MAAM,CAAC,UAAU,CAAA;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,KAAK,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CAAA;CAChD;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,uBAAuB,GAAG,sBAAsB,CAAC,
|
|
1
|
+
{"version":3,"file":"admin-service.d.ts","sourceRoot":"","sources":["../../../src/admin/server/admin-service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,MAAM,MAAM,sBAAsB,CAAA;AAE9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAM5C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC,UAAU,CAAA;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,IAAI,CAAA;IACb,KAAK,EAAE,MAAM,CAAC,UAAU,CAAA;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,KAAK,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CAAA;CAChD;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,uBAAuB,GAAG,sBAAsB,CAAC,CA0C3D;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,OAAO,GAAG,OAAiB,GAChC,OAAO,CAAC,iBAAiB,CAAC,CA0B5B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAwB9B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,CAUnC;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,CAUnC;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC,OAAO,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,CAyBnC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGtE;AAED;;;GAGG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAQnD"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Admin CRUD operations.
|
|
5
5
|
* Each store has its own database (via Neon branches) - no tenant isolation needed.
|
|
6
6
|
*/
|
|
7
|
-
import { eq } from 'drizzle-orm';
|
|
7
|
+
import { eq, sql } from 'drizzle-orm';
|
|
8
8
|
import { getDb } from '../../core/db/client';
|
|
9
9
|
import * as schema from '../../core/db/schema';
|
|
10
10
|
import { hashPassword, verifyPassword } from '../../auth/server/password';
|
|
@@ -43,6 +43,18 @@ export async function authenticateAdmin(email, password) {
|
|
|
43
43
|
code: 'INVALID_CREDENTIALS',
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
// Block 'deactivated' and 'invited' admins. 'invited' admins have no
|
|
47
|
+
// password hash yet (they must accept their invite first); 'deactivated'
|
|
48
|
+
// admins are blocked entirely. Return the same generic error message as
|
|
49
|
+
// wrong-password so we don't reveal account state.
|
|
50
|
+
const adminStatus = admin.status ?? 'active';
|
|
51
|
+
if (adminStatus !== 'active' || !admin.passwordHash) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: 'Invalid email or password',
|
|
55
|
+
code: 'INVALID_CREDENTIALS',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
46
58
|
// Verify password
|
|
47
59
|
const isValidPassword = await verifyPassword(password, admin.passwordHash);
|
|
48
60
|
if (!isValidPassword) {
|
|
@@ -215,9 +227,16 @@ export async function updateAdmin(adminId, data) {
|
|
|
215
227
|
export async function updateAdminPassword(adminId, newPassword) {
|
|
216
228
|
const db = getDb();
|
|
217
229
|
const passwordHash = await hashPassword(newPassword);
|
|
230
|
+
// Bump session_version atomically so all existing JWTs for this admin
|
|
231
|
+
// are invalidated on their next request. Every caller (self-service,
|
|
232
|
+
// forgot-password reset, accept-invite, emergency reset) gets this
|
|
233
|
+
// invalidation semantics consistently.
|
|
218
234
|
await db
|
|
219
235
|
.update(schema.storeAdmins)
|
|
220
|
-
.set({
|
|
236
|
+
.set({
|
|
237
|
+
passwordHash,
|
|
238
|
+
sessionVersion: sql `${schema.storeAdmins.sessionVersion} + 1`,
|
|
239
|
+
})
|
|
221
240
|
.where(eq(schema.storeAdmins.id, adminId));
|
|
222
241
|
}
|
|
223
242
|
/**
|