@molecule/api-resource-user 1.0.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/LICENSE +115 -0
- package/dist/authorization.d.ts +107 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +375 -0
- package/dist/authorization.js.map +1 -0
- package/dist/authorizers/auth.d.ts +8 -0
- package/dist/authorizers/auth.d.ts.map +1 -0
- package/dist/authorizers/auth.js +19 -0
- package/dist/authorizers/auth.js.map +1 -0
- package/dist/authorizers/authSelf.d.ts +14 -0
- package/dist/authorizers/authSelf.d.ts.map +1 -0
- package/dist/authorizers/authSelf.js +31 -0
- package/dist/authorizers/authSelf.js.map +1 -0
- package/dist/authorizers/index.d.ts +5 -0
- package/dist/authorizers/index.d.ts.map +1 -0
- package/dist/authorizers/index.js +5 -0
- package/dist/authorizers/index.js.map +1 -0
- package/dist/authorizers/rateLimit.d.ts +84 -0
- package/dist/authorizers/rateLimit.d.ts.map +1 -0
- package/dist/authorizers/rateLimit.js +144 -0
- package/dist/authorizers/rateLimit.js.map +1 -0
- package/dist/authorizers/requireWebhookAuthenticity.d.ts +23 -0
- package/dist/authorizers/requireWebhookAuthenticity.d.ts.map +1 -0
- package/dist/authorizers/requireWebhookAuthenticity.js +76 -0
- package/dist/authorizers/requireWebhookAuthenticity.js.map +1 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +19 -0
- package/dist/browser-guard.js.map +1 -0
- package/dist/handlers/create.d.ts +52 -0
- package/dist/handlers/create.d.ts.map +1 -0
- package/dist/handlers/create.js +208 -0
- package/dist/handlers/create.js.map +1 -0
- package/dist/handlers/del.d.ts +30 -0
- package/dist/handlers/del.d.ts.map +1 -0
- package/dist/handlers/del.js +55 -0
- package/dist/handlers/del.js.map +1 -0
- package/dist/handlers/forgotPassword.d.ts +34 -0
- package/dist/handlers/forgotPassword.d.ts.map +1 -0
- package/dist/handlers/forgotPassword.js +119 -0
- package/dist/handlers/forgotPassword.js.map +1 -0
- package/dist/handlers/index.d.ts +16 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +16 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/logIn.d.ts +91 -0
- package/dist/handlers/logIn.d.ts.map +1 -0
- package/dist/handlers/logIn.js +223 -0
- package/dist/handlers/logIn.js.map +1 -0
- package/dist/handlers/logInOAuth.d.ts +66 -0
- package/dist/handlers/logInOAuth.d.ts.map +1 -0
- package/dist/handlers/logInOAuth.js +462 -0
- package/dist/handlers/logInOAuth.js.map +1 -0
- package/dist/handlers/logout.d.ts +22 -0
- package/dist/handlers/logout.d.ts.map +1 -0
- package/dist/handlers/logout.js +57 -0
- package/dist/handlers/logout.js.map +1 -0
- package/dist/handlers/oauthAuthorize.d.ts +44 -0
- package/dist/handlers/oauthAuthorize.d.ts.map +1 -0
- package/dist/handlers/oauthAuthorize.js +141 -0
- package/dist/handlers/oauthAuthorize.js.map +1 -0
- package/dist/handlers/payments/handlePaymentNotification.d.ts +28 -0
- package/dist/handlers/payments/handlePaymentNotification.d.ts.map +1 -0
- package/dist/handlers/payments/handlePaymentNotification.js +217 -0
- package/dist/handlers/payments/handlePaymentNotification.js.map +1 -0
- package/dist/handlers/payments/registry.d.ts +13 -0
- package/dist/handlers/payments/registry.d.ts.map +1 -0
- package/dist/handlers/payments/registry.js +13 -0
- package/dist/handlers/payments/registry.js.map +1 -0
- package/dist/handlers/payments/verifyPayment.d.ts +37 -0
- package/dist/handlers/payments/verifyPayment.d.ts.map +1 -0
- package/dist/handlers/payments/verifyPayment.js +383 -0
- package/dist/handlers/payments/verifyPayment.js.map +1 -0
- package/dist/handlers/read.d.ts +17 -0
- package/dist/handlers/read.d.ts.map +1 -0
- package/dist/handlers/read.js +60 -0
- package/dist/handlers/read.js.map +1 -0
- package/dist/handlers/readSelf.d.ts +23 -0
- package/dist/handlers/readSelf.d.ts.map +1 -0
- package/dist/handlers/readSelf.js +77 -0
- package/dist/handlers/readSelf.js.map +1 -0
- package/dist/handlers/resetPassword.d.ts +40 -0
- package/dist/handlers/resetPassword.d.ts.map +1 -0
- package/dist/handlers/resetPassword.js +147 -0
- package/dist/handlers/resetPassword.js.map +1 -0
- package/dist/handlers/update.d.ts +28 -0
- package/dist/handlers/update.d.ts.map +1 -0
- package/dist/handlers/update.js +250 -0
- package/dist/handlers/update.js.map +1 -0
- package/dist/handlers/updatePassword.d.ts +35 -0
- package/dist/handlers/updatePassword.d.ts.map +1 -0
- package/dist/handlers/updatePassword.js +113 -0
- package/dist/handlers/updatePassword.js.map +1 -0
- package/dist/handlers/updatePlan.d.ts +41 -0
- package/dist/handlers/updatePlan.d.ts.map +1 -0
- package/dist/handlers/updatePlan.js +242 -0
- package/dist/handlers/updatePlan.js.map +1 -0
- package/dist/handlers/verifyTwoFactor.d.ts +46 -0
- package/dist/handlers/verifyTwoFactor.d.ts.map +1 -0
- package/dist/handlers/verifyTwoFactor.js +162 -0
- package/dist/handlers/verifyTwoFactor.js.map +1 -0
- package/dist/i18n.d.ts +10 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +25 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/requestHandlerMap.d.ts +44 -0
- package/dist/requestHandlerMap.d.ts.map +1 -0
- package/dist/requestHandlerMap.js +68 -0
- package/dist/requestHandlerMap.js.map +1 -0
- package/dist/resource.d.ts +22 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +23 -0
- package/dist/resource.js.map +1 -0
- package/dist/routes.d.ts +44 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +121 -0
- package/dist/routes.js.map +1 -0
- package/dist/schema.d.ts +193 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +229 -0
- package/dist/schema.js.map +1 -0
- package/dist/secrets.d.ts +16 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +28 -0
- package/dist/secrets.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utilities/getPlan.d.ts +19 -0
- package/dist/utilities/getPlan.d.ts.map +1 -0
- package/dist/utilities/getPlan.js +26 -0
- package/dist/utilities/getPlan.js.map +1 -0
- package/dist/utilities/hashResetToken.d.ts +16 -0
- package/dist/utilities/hashResetToken.d.ts.map +1 -0
- package/dist/utilities/hashResetToken.js +19 -0
- package/dist/utilities/hashResetToken.js.map +1 -0
- package/dist/utilities/index.d.ts +5 -0
- package/dist/utilities/index.d.ts.map +1 -0
- package/dist/utilities/index.js +5 -0
- package/dist/utilities/index.js.map +1 -0
- package/dist/utilities/invalidateEntitlements.d.ts +24 -0
- package/dist/utilities/invalidateEntitlements.d.ts.map +1 -0
- package/dist/utilities/invalidateEntitlements.js +41 -0
- package/dist/utilities/invalidateEntitlements.js.map +1 -0
- package/dist/utilities/normalizeEmail.d.ts +22 -0
- package/dist/utilities/normalizeEmail.d.ts.map +1 -0
- package/dist/utilities/normalizeEmail.js +28 -0
- package/dist/utilities/normalizeEmail.js.map +1 -0
- package/dist/utilities/notify.d.ts +21 -0
- package/dist/utilities/notify.d.ts.map +1 -0
- package/dist/utilities/notify.js +39 -0
- package/dist/utilities/notify.js.map +1 -0
- package/dist/utilities/stripSensitiveUserColumns.d.ts +25 -0
- package/dist/utilities/stripSensitiveUserColumns.d.ts.map +1 -0
- package/dist/utilities/stripSensitiveUserColumns.js +43 -0
- package/dist/utilities/stripSensitiveUserColumns.js.map +1 -0
- package/package.json +94 -0
- package/setup/users.sql +22 -0
- package/setup/usersSecrets.sql +9 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { get, getAnalytics, getLogger } from '@molecule/api-bond';
|
|
3
|
+
import { findOne, updateById } from '@molecule/api-database';
|
|
4
|
+
import { t } from '@molecule/api-i18n';
|
|
5
|
+
import { hash } from '@molecule/api-password';
|
|
6
|
+
import { invalidateAllDeviceExistsCache } from '../authorization.js';
|
|
7
|
+
import { hashResetToken } from '../utilities/hashResetToken.js';
|
|
8
|
+
const analytics = getAnalytics();
|
|
9
|
+
const logger = getLogger();
|
|
10
|
+
const RESET_TOKEN_TTL_MS = 1000 * 60 * 60;
|
|
11
|
+
/**
|
|
12
|
+
* Confirms a password reset by validating the one-time token previously generated by
|
|
13
|
+
* `forgotPassword`, then hashing and storing the new password and clearing the token.
|
|
14
|
+
*
|
|
15
|
+
* Token TTL is one hour. The handler responds with 400 for missing fields, 400 for
|
|
16
|
+
* passwords that fail length validation (matching `updatePassword`), and 401 for
|
|
17
|
+
* tokens that are unknown, expired, or already consumed.
|
|
18
|
+
*
|
|
19
|
+
* On success, all existing device sessions are invalidated to force a fresh login,
|
|
20
|
+
* mirroring the behavior of `updatePassword`.
|
|
21
|
+
*
|
|
22
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
23
|
+
* @param resource.tableName - The database table name for users.
|
|
24
|
+
* @returns A request handler that responds with `{ statusCode: 200, body: { success: true } }` on success.
|
|
25
|
+
*/
|
|
26
|
+
export const resetPassword = ({ name: _name, tableName, schema: _schema }) => {
|
|
27
|
+
return async (req) => {
|
|
28
|
+
try {
|
|
29
|
+
const { token, password } = req.body;
|
|
30
|
+
if (!token) {
|
|
31
|
+
return {
|
|
32
|
+
statusCode: 400,
|
|
33
|
+
body: {
|
|
34
|
+
error: t('user.error.tokenRequired'),
|
|
35
|
+
errorKey: 'user.error.tokenRequired',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (!password) {
|
|
40
|
+
return {
|
|
41
|
+
statusCode: 400,
|
|
42
|
+
body: {
|
|
43
|
+
error: t('user.error.newPasswordRequired'),
|
|
44
|
+
errorKey: 'user.error.newPasswordRequired',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (password.length < 8) {
|
|
49
|
+
return {
|
|
50
|
+
statusCode: 400,
|
|
51
|
+
body: {
|
|
52
|
+
error: t('user.error.passwordTooShort', undefined, {
|
|
53
|
+
defaultValue: 'Password must be at least 8 characters',
|
|
54
|
+
}),
|
|
55
|
+
errorKey: 'user.error.passwordTooShort',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (password.length > 1024) {
|
|
60
|
+
return {
|
|
61
|
+
statusCode: 400,
|
|
62
|
+
body: {
|
|
63
|
+
error: t('user.error.passwordTooLong', undefined, {
|
|
64
|
+
defaultValue: 'Password must be at most 1024 characters',
|
|
65
|
+
}),
|
|
66
|
+
errorKey: 'user.error.passwordTooLong',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// The stored value is sha256(token); look up + compare by that digest.
|
|
71
|
+
const tokenHash = hashResetToken(token);
|
|
72
|
+
const secrets = await findOne(`${tableName}Secrets`, [
|
|
73
|
+
{ field: 'passwordResetToken', operator: '=', value: tokenHash },
|
|
74
|
+
]);
|
|
75
|
+
if (!secrets || !secrets.passwordResetToken || !secrets.passwordResetTokenAt) {
|
|
76
|
+
return {
|
|
77
|
+
statusCode: 401,
|
|
78
|
+
body: {
|
|
79
|
+
error: t('user.error.invalidToken'),
|
|
80
|
+
errorKey: 'user.error.invalidToken',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Constant-time comparison to defeat timing attacks even though the lookup
|
|
85
|
+
// already matched — defense in depth.
|
|
86
|
+
const a = Buffer.from(tokenHash, 'utf-8');
|
|
87
|
+
const b = Buffer.from(secrets.passwordResetToken, 'utf-8');
|
|
88
|
+
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
|
|
89
|
+
return {
|
|
90
|
+
statusCode: 401,
|
|
91
|
+
body: {
|
|
92
|
+
error: t('user.error.invalidToken'),
|
|
93
|
+
errorKey: 'user.error.invalidToken',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const tokenAge = Date.now() - new Date(secrets.passwordResetTokenAt).getTime();
|
|
98
|
+
if (!Number.isFinite(tokenAge) || tokenAge > RESET_TOKEN_TTL_MS || tokenAge < 0) {
|
|
99
|
+
return {
|
|
100
|
+
statusCode: 401,
|
|
101
|
+
body: {
|
|
102
|
+
error: t('user.error.invalidToken'),
|
|
103
|
+
errorKey: 'user.error.invalidToken',
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const userId = secrets.id;
|
|
108
|
+
if (!userId) {
|
|
109
|
+
return {
|
|
110
|
+
statusCode: 500,
|
|
111
|
+
body: {
|
|
112
|
+
error: t('user.error.failedToUpdatePassword'),
|
|
113
|
+
errorKey: 'user.error.failedToUpdatePassword',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const passwordHash = await hash(password);
|
|
118
|
+
await updateById(`${tableName}Secrets`, userId, {
|
|
119
|
+
passwordHash,
|
|
120
|
+
passwordResetToken: null,
|
|
121
|
+
passwordResetTokenAt: null,
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
await get('device')?.deleteByUserId(userId);
|
|
125
|
+
// Evict this process's positive device-exists cache so no just-deleted
|
|
126
|
+
// device keeps authenticating for the cache TTL — immediate revocation.
|
|
127
|
+
invalidateAllDeviceExistsCache();
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
logger.warn('Failed to invalidate sessions after password reset', { userId, error });
|
|
131
|
+
}
|
|
132
|
+
analytics.track({ name: 'user.password_reset_completed', userId }).catch(() => { });
|
|
133
|
+
return { statusCode: 200, body: { success: true } };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
logger.error(error);
|
|
137
|
+
return {
|
|
138
|
+
statusCode: 500,
|
|
139
|
+
body: {
|
|
140
|
+
error: t('user.error.failedToUpdatePassword'),
|
|
141
|
+
errorKey: 'user.error.failedToUpdatePassword',
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=resetPassword.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resetPassword.js","sourceRoot":"","sources":["../../src/handlers/resetPassword.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MoleculeRequest } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Updates a user's profile fields (username, name, email). Validates username format
|
|
5
|
+
* (alphanumeric, max 255 chars), email format, and uniqueness of both fields against
|
|
6
|
+
* other users before delegating to the resource update utility.
|
|
7
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
8
|
+
* @param resource.name - The resource name.
|
|
9
|
+
* @param resource.tableName - The database table name for users.
|
|
10
|
+
* @param resource.schema - The validation schema for user properties.
|
|
11
|
+
* @returns A request handler that responds with the updated user props on success.
|
|
12
|
+
*/
|
|
13
|
+
export declare const update: ({ name, tableName, schema: _schema }: types.Resource) => (req: MoleculeRequest) => Promise<{
|
|
14
|
+
statusCode: number;
|
|
15
|
+
body: {
|
|
16
|
+
error: string;
|
|
17
|
+
errorKey: string;
|
|
18
|
+
props?: undefined;
|
|
19
|
+
};
|
|
20
|
+
} | {
|
|
21
|
+
statusCode: number;
|
|
22
|
+
body: {
|
|
23
|
+
props: Pick<import("@molecule/api-resource/types").Props, "updatedAt"> & Record<string, unknown>;
|
|
24
|
+
error?: undefined;
|
|
25
|
+
errorKey?: undefined;
|
|
26
|
+
};
|
|
27
|
+
}>;
|
|
28
|
+
//# sourceMappingURL=update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/handlers/update.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAI7D,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AA0BzC;;;;;;;;;GASG;AACH,eAAO,MAAM,MAAM,GAAI,sCAAsC,KAAK,CAAC,QAAQ,MAO3D,KAAK,eAAe;;;;;aArBL,CAAA;;;;;;;;;EAyP9B,CAAA"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { getAnalytics, getLogger } from '@molecule/api-bond';
|
|
2
|
+
import { findOne } from '@molecule/api-database';
|
|
3
|
+
const logger = getLogger();
|
|
4
|
+
const analytics = getAnalytics();
|
|
5
|
+
import { t } from '@molecule/api-i18n';
|
|
6
|
+
import { compare } from '@molecule/api-password';
|
|
7
|
+
import { update as resourceUpdate } from '@molecule/api-resource';
|
|
8
|
+
import { MAX_AVATAR_LENGTH, MAX_BIO_LENGTH, propsSchema, updatePropsSchema } from '../schema.js';
|
|
9
|
+
import { normalizeEmail } from '../utilities/normalizeEmail.js';
|
|
10
|
+
/**
|
|
11
|
+
* Extended update schema that also permits the application-specific
|
|
12
|
+
* `oauthData` bag to flow through the standard `PATCH /api/users/:id`
|
|
13
|
+
* endpoint. Apps frequently store per-user UI/feature preferences inside
|
|
14
|
+
* `oauthData` (it is the only freeform JSON field on the user row), so
|
|
15
|
+
* the update handler needs to accept and persist it.
|
|
16
|
+
*
|
|
17
|
+
* Username/name/email retain their existing validation paths above.
|
|
18
|
+
*/
|
|
19
|
+
const extendedUpdatePropsSchema = updatePropsSchema.extend({
|
|
20
|
+
oauthData: propsSchema.shape.oauthData,
|
|
21
|
+
// emailVerified is permitted through the schema ONLY so the handler can RESET it
|
|
22
|
+
// to false when the email changes (server-set below). It is NEVER copied from
|
|
23
|
+
// req.body — the same discipline as twoFactorEnabled — so a caller cannot
|
|
24
|
+
// self-verify by sending `emailVerified: true`.
|
|
25
|
+
emailVerified: propsSchema.shape.emailVerified,
|
|
26
|
+
// [M6-1] twoFactorEnabled is deliberately NOT writable here. The generic profile update is
|
|
27
|
+
// gated only by authSelf (no TOTP step-up), so allowing it would let a caller PATCH 2FA off
|
|
28
|
+
// without the current second factor — defeating verifyTwoFactor's disable step-up. ALL 2FA
|
|
29
|
+
// state transitions must go through verifyTwoFactor (enable/disable), which verifies the
|
|
30
|
+
// live TOTP token and updates the secret atomically.
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Updates a user's profile fields (username, name, email). Validates username format
|
|
34
|
+
* (alphanumeric, max 255 chars), email format, and uniqueness of both fields against
|
|
35
|
+
* other users before delegating to the resource update utility.
|
|
36
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
37
|
+
* @param resource.name - The resource name.
|
|
38
|
+
* @param resource.tableName - The database table name for users.
|
|
39
|
+
* @param resource.schema - The validation schema for user properties.
|
|
40
|
+
* @returns A request handler that responds with the updated user props on success.
|
|
41
|
+
*/
|
|
42
|
+
export const update = ({ name, tableName, schema: _schema }) => {
|
|
43
|
+
const updateResource = resourceUpdate({
|
|
44
|
+
name,
|
|
45
|
+
tableName,
|
|
46
|
+
schema: extendedUpdatePropsSchema,
|
|
47
|
+
});
|
|
48
|
+
return async (req) => {
|
|
49
|
+
try {
|
|
50
|
+
const id = req.params.id;
|
|
51
|
+
const props = {};
|
|
52
|
+
if (req.body.username !== undefined) {
|
|
53
|
+
props.username = String(req.body.username)
|
|
54
|
+
.replace(/[^a-zA-Z0-9]/g, '')
|
|
55
|
+
.substring(0, 255);
|
|
56
|
+
if (!props.username) {
|
|
57
|
+
return {
|
|
58
|
+
statusCode: 400,
|
|
59
|
+
body: {
|
|
60
|
+
error: t('user.error.usernameCannotBeEmpty'),
|
|
61
|
+
errorKey: 'user.error.usernameCannotBeEmpty',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Check if username is taken by another user.
|
|
66
|
+
const existing = await findOne(tableName, [
|
|
67
|
+
{ field: 'username', operator: '=', value: props.username },
|
|
68
|
+
{ field: 'id', operator: '!=', value: id },
|
|
69
|
+
]);
|
|
70
|
+
if (existing) {
|
|
71
|
+
return {
|
|
72
|
+
statusCode: 403,
|
|
73
|
+
body: {
|
|
74
|
+
error: t('user.error.usernameUnavailable'),
|
|
75
|
+
errorKey: 'user.error.usernameUnavailable',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (req.body.name !== undefined) {
|
|
81
|
+
props.name = String(req.body.name).substring(0, 255);
|
|
82
|
+
}
|
|
83
|
+
if (req.body.email !== undefined) {
|
|
84
|
+
let nextEmail;
|
|
85
|
+
if (req.body.email === '' || req.body.email === null) {
|
|
86
|
+
nextEmail = null;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Normalize (lowercase) on store to match signup + the login/reset
|
|
90
|
+
// lookups — otherwise a profile email change stores a mixed-case value
|
|
91
|
+
// that those normalized lookups can't find, and lets a case-variant
|
|
92
|
+
// duplicate slip past the uniqueness check below.
|
|
93
|
+
nextEmail = (normalizeEmail(String(req.body.email)) ?? '').substring(0, 1023);
|
|
94
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(nextEmail)) {
|
|
95
|
+
return {
|
|
96
|
+
statusCode: 400,
|
|
97
|
+
body: { error: t('user.error.emailInvalid'), errorKey: 'user.error.emailInvalid' },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Check if email is taken by another user.
|
|
101
|
+
const existing = await findOne(tableName, [
|
|
102
|
+
{ field: 'email', operator: '=', value: nextEmail },
|
|
103
|
+
{ field: 'id', operator: '!=', value: id },
|
|
104
|
+
]);
|
|
105
|
+
if (existing) {
|
|
106
|
+
return {
|
|
107
|
+
statusCode: 403,
|
|
108
|
+
body: {
|
|
109
|
+
error: t('user.error.emailAlreadyRegistered'),
|
|
110
|
+
errorKey: 'user.error.emailAlreadyRegistered',
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const current = await findOne(tableName, [
|
|
116
|
+
{ field: 'id', operator: '=', value: id },
|
|
117
|
+
]);
|
|
118
|
+
const changingEmail = !current || (current.email ?? null) !== nextEmail;
|
|
119
|
+
if (changingEmail) {
|
|
120
|
+
// Re-authentication gate: the email is the account-recovery anchor
|
|
121
|
+
// (password reset goes to it), so changing OR clearing it with a
|
|
122
|
+
// hijacked session must not be silent. Accounts WITH a password must
|
|
123
|
+
// present it (same contract + i18n keys as updatePassword); OAuth-only
|
|
124
|
+
// accounts have no password to present, so their (provider-backed)
|
|
125
|
+
// session remains the credential.
|
|
126
|
+
const secrets = await findOne(`${tableName}Secrets`, [
|
|
127
|
+
{ field: 'id', operator: '=', value: id },
|
|
128
|
+
]);
|
|
129
|
+
if (secrets?.passwordHash) {
|
|
130
|
+
const currentPassword = typeof req.body.currentPassword === 'string' ? req.body.currentPassword : '';
|
|
131
|
+
if (!currentPassword) {
|
|
132
|
+
return {
|
|
133
|
+
statusCode: 400,
|
|
134
|
+
body: {
|
|
135
|
+
error: t('user.error.currentPasswordRequired'),
|
|
136
|
+
errorKey: 'user.error.currentPasswordRequired',
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (!(await compare(currentPassword, secrets.passwordHash))) {
|
|
141
|
+
return {
|
|
142
|
+
statusCode: 403,
|
|
143
|
+
body: {
|
|
144
|
+
error: t('user.error.currentPasswordIncorrect'),
|
|
145
|
+
errorKey: 'user.error.currentPasswordIncorrect',
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Changing the address invalidates the verified flag — the new email is
|
|
151
|
+
// unproven, and leaving emailVerified=true would let a caller point a
|
|
152
|
+
// "verified" email anywhere (poisoning password-reset and email-gated
|
|
153
|
+
// access). Reset ONLY on a real change so an unchanged profile save does
|
|
154
|
+
// not needlessly un-verify. Server-set — never read from req.body.
|
|
155
|
+
props.emailVerified = false;
|
|
156
|
+
}
|
|
157
|
+
props.email = nextEmail;
|
|
158
|
+
}
|
|
159
|
+
if (req.body.avatar !== undefined) {
|
|
160
|
+
// `null` or '' clears the avatar; anything else is coerced to a string and length-capped.
|
|
161
|
+
if (req.body.avatar === null || req.body.avatar === '') {
|
|
162
|
+
props.avatar = null;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const avatar = String(req.body.avatar);
|
|
166
|
+
if (avatar.length > MAX_AVATAR_LENGTH) {
|
|
167
|
+
return {
|
|
168
|
+
statusCode: 400,
|
|
169
|
+
body: {
|
|
170
|
+
error: t('user.error.avatarTooLarge', { max: MAX_AVATAR_LENGTH }, {
|
|
171
|
+
defaultValue: 'Avatar must be {{max}} characters or fewer.',
|
|
172
|
+
}),
|
|
173
|
+
errorKey: 'user.error.avatarTooLarge',
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
props.avatar = avatar;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (req.body.bio !== undefined) {
|
|
181
|
+
// `null` clears the bio; otherwise coerce to a string and length-cap (empty string is allowed).
|
|
182
|
+
if (req.body.bio === null) {
|
|
183
|
+
props.bio = null;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const bio = String(req.body.bio);
|
|
187
|
+
if (bio.length > MAX_BIO_LENGTH) {
|
|
188
|
+
return {
|
|
189
|
+
statusCode: 400,
|
|
190
|
+
body: {
|
|
191
|
+
error: t('user.error.bioTooLong', { max: MAX_BIO_LENGTH }, {
|
|
192
|
+
defaultValue: 'Bio must be {{max}} characters or fewer.',
|
|
193
|
+
}),
|
|
194
|
+
errorKey: 'user.error.bioTooLong',
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
props.bio = bio;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// [M6-1] twoFactorEnabled is intentionally NOT copied from the request body — see the
|
|
202
|
+
// extendedUpdatePropsSchema note above. 2FA toggles go through verifyTwoFactor only.
|
|
203
|
+
if (req.body.oauthData !== undefined) {
|
|
204
|
+
if (req.body.oauthData === null || typeof req.body.oauthData !== 'object') {
|
|
205
|
+
return {
|
|
206
|
+
statusCode: 400,
|
|
207
|
+
body: {
|
|
208
|
+
error: t('user.error.oauthDataInvalid', undefined, {
|
|
209
|
+
defaultValue: 'oauthData must be an object.',
|
|
210
|
+
}),
|
|
211
|
+
errorKey: 'user.error.oauthDataInvalid',
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
props.oauthData = req.body.oauthData;
|
|
216
|
+
}
|
|
217
|
+
if (Object.keys(props).length === 0) {
|
|
218
|
+
return {
|
|
219
|
+
statusCode: 400,
|
|
220
|
+
body: {
|
|
221
|
+
error: t('user.error.noUpdatableFields', undefined, {
|
|
222
|
+
defaultValue: 'No updatable fields were provided.',
|
|
223
|
+
}),
|
|
224
|
+
errorKey: 'user.error.noUpdatableFields',
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const result = await updateResource({ id: id, props });
|
|
229
|
+
analytics
|
|
230
|
+
.track({
|
|
231
|
+
name: 'user.profile_updated',
|
|
232
|
+
userId: id,
|
|
233
|
+
properties: { fields: Object.keys(props) },
|
|
234
|
+
})
|
|
235
|
+
.catch(() => { });
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
logger.error(error);
|
|
240
|
+
return {
|
|
241
|
+
statusCode: 500,
|
|
242
|
+
body: {
|
|
243
|
+
error: t('user.error.failedToUpdateUser'),
|
|
244
|
+
errorKey: 'user.error.failedToUpdateUser',
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
//# sourceMappingURL=update.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.js","sourceRoot":"","sources":["../../src/handlers/update.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MoleculeRequest } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/** Request body for password update, with current password verification and new password. */
|
|
4
|
+
export interface UpdatePasswordRequest extends MoleculeRequest {
|
|
5
|
+
body: {
|
|
6
|
+
currentPassword?: string;
|
|
7
|
+
newPassword?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Updates a user's password. If the user already has a password hash, the current password
|
|
12
|
+
* must be provided and verified first. The new password is hashed via `@molecule/api-password`
|
|
13
|
+
* and stored in the secrets table.
|
|
14
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
15
|
+
* @param resource.name - The resource name.
|
|
16
|
+
* @param resource.tableName - The database table name for users.
|
|
17
|
+
* @param resource.schema - The validation schema for user properties.
|
|
18
|
+
* @returns A request handler that responds with `{ statusCode: 200, body: { success: true } }` on success.
|
|
19
|
+
*/
|
|
20
|
+
export declare const updatePassword: ({ name: _name, tableName, schema: _schema }: types.Resource) => (req: UpdatePasswordRequest) => Promise<{
|
|
21
|
+
statusCode: number;
|
|
22
|
+
body: {
|
|
23
|
+
error: string;
|
|
24
|
+
errorKey: string;
|
|
25
|
+
success?: undefined;
|
|
26
|
+
};
|
|
27
|
+
} | {
|
|
28
|
+
statusCode: number;
|
|
29
|
+
body: {
|
|
30
|
+
success: boolean;
|
|
31
|
+
error?: undefined;
|
|
32
|
+
errorKey?: undefined;
|
|
33
|
+
};
|
|
34
|
+
}>;
|
|
35
|
+
//# sourceMappingURL=updatePassword.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updatePassword.d.ts","sourceRoot":"","sources":["../../src/handlers/updatePassword.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAG7D,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAKzC,6FAA6F;AAC7F,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,EAAE;QACJ,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,WAAW,CAAC,EAAE,MAAM,CAAA;KACrB,CAAA;CACF;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,6CAA6C,KAAK,CAAC,QAAQ,MAC1E,KAAK,qBAAqB;;;;;;;;;;;;;;EAqGzC,CAAA"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { get, getAnalytics, getLogger } from '@molecule/api-bond';
|
|
2
|
+
import { findById, updateById } from '@molecule/api-database';
|
|
3
|
+
import { t } from '@molecule/api-i18n';
|
|
4
|
+
import { compare, hash } from '@molecule/api-password';
|
|
5
|
+
import { invalidateAllDeviceExistsCache } from '../authorization.js';
|
|
6
|
+
const analytics = getAnalytics();
|
|
7
|
+
const logger = getLogger();
|
|
8
|
+
/**
|
|
9
|
+
* Updates a user's password. If the user already has a password hash, the current password
|
|
10
|
+
* must be provided and verified first. The new password is hashed via `@molecule/api-password`
|
|
11
|
+
* and stored in the secrets table.
|
|
12
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
13
|
+
* @param resource.name - The resource name.
|
|
14
|
+
* @param resource.tableName - The database table name for users.
|
|
15
|
+
* @param resource.schema - The validation schema for user properties.
|
|
16
|
+
* @returns A request handler that responds with `{ statusCode: 200, body: { success: true } }` on success.
|
|
17
|
+
*/
|
|
18
|
+
export const updatePassword = ({ name: _name, tableName, schema: _schema }) => {
|
|
19
|
+
return async (req) => {
|
|
20
|
+
try {
|
|
21
|
+
const id = req.params.id;
|
|
22
|
+
const { currentPassword, newPassword } = req.body;
|
|
23
|
+
if (!newPassword) {
|
|
24
|
+
return {
|
|
25
|
+
statusCode: 400,
|
|
26
|
+
body: {
|
|
27
|
+
error: t('user.error.newPasswordRequired'),
|
|
28
|
+
errorKey: 'user.error.newPasswordRequired',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (newPassword.length < 8) {
|
|
33
|
+
return {
|
|
34
|
+
statusCode: 400,
|
|
35
|
+
body: {
|
|
36
|
+
error: t('user.error.passwordTooShort', undefined, {
|
|
37
|
+
defaultValue: 'Password must be at least 8 characters',
|
|
38
|
+
}),
|
|
39
|
+
errorKey: 'user.error.passwordTooShort',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (newPassword.length > 1024) {
|
|
44
|
+
return {
|
|
45
|
+
statusCode: 400,
|
|
46
|
+
body: {
|
|
47
|
+
error: t('user.error.passwordTooLong', undefined, {
|
|
48
|
+
defaultValue: 'Password must be at most 1024 characters',
|
|
49
|
+
}),
|
|
50
|
+
errorKey: 'user.error.passwordTooLong',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Get current password hash.
|
|
55
|
+
const secrets = await findById(`${tableName}Secrets`, id);
|
|
56
|
+
// If user has a password, verify the current one.
|
|
57
|
+
if (secrets?.passwordHash) {
|
|
58
|
+
if (!currentPassword) {
|
|
59
|
+
return {
|
|
60
|
+
statusCode: 400,
|
|
61
|
+
body: {
|
|
62
|
+
error: t('user.error.currentPasswordRequired'),
|
|
63
|
+
errorKey: 'user.error.currentPasswordRequired',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const isValid = await compare(currentPassword, secrets.passwordHash);
|
|
68
|
+
if (!isValid) {
|
|
69
|
+
return {
|
|
70
|
+
statusCode: 403,
|
|
71
|
+
body: {
|
|
72
|
+
error: t('user.error.currentPasswordIncorrect'),
|
|
73
|
+
errorKey: 'user.error.currentPasswordIncorrect',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Hash and store the new password, and clear any outstanding reset token.
|
|
79
|
+
const passwordHash = await hash(newPassword);
|
|
80
|
+
await updateById(`${tableName}Secrets`, id, {
|
|
81
|
+
passwordHash,
|
|
82
|
+
passwordResetToken: null,
|
|
83
|
+
passwordResetTokenAt: null,
|
|
84
|
+
});
|
|
85
|
+
// Invalidate all existing sessions by deleting all devices for this user.
|
|
86
|
+
// This forces re-login on every device, preventing compromised sessions
|
|
87
|
+
// from persisting after a password change.
|
|
88
|
+
try {
|
|
89
|
+
await get('device')?.deleteByUserId(id);
|
|
90
|
+
// Evict this process's positive device-exists cache so no just-deleted
|
|
91
|
+
// device keeps authenticating for the cache TTL — immediate revocation.
|
|
92
|
+
invalidateAllDeviceExistsCache();
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Non-critical — password was already changed successfully
|
|
96
|
+
logger.warn('Failed to invalidate sessions after password change', { error, userId: id });
|
|
97
|
+
}
|
|
98
|
+
analytics.track({ name: 'user.password_changed', userId: id }).catch(() => { });
|
|
99
|
+
return { statusCode: 200, body: { success: true } };
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
logger.error(error);
|
|
103
|
+
return {
|
|
104
|
+
statusCode: 500,
|
|
105
|
+
body: {
|
|
106
|
+
error: t('user.error.failedToUpdatePassword'),
|
|
107
|
+
errorKey: 'user.error.failedToUpdatePassword',
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=updatePassword.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updatePassword.js","sourceRoot":"","sources":["../../src/handlers/updatePassword.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MoleculeRequest } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/** Request body for plan update, containing the target plan key. */
|
|
4
|
+
export interface UpdatePlanRequest extends MoleculeRequest {
|
|
5
|
+
body: {
|
|
6
|
+
planKey?: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Updates a user's subscription plan. Uses the bonded PlanService to look up plan metadata and
|
|
11
|
+
* delegates to the appropriate bonded PaymentProvider based on the plan's `platformKey`. Handles
|
|
12
|
+
* cross-platform subscription cancellation when switching providers. Returns a checkout URL
|
|
13
|
+
* (status 201) for new subscriptions, or directly updates the user record for in-place changes
|
|
14
|
+
* and free plan downgrades.
|
|
15
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
16
|
+
* @param resource.name - The resource name.
|
|
17
|
+
* @param resource.tableName - The database table name for users.
|
|
18
|
+
* @param resource.schema - The validation schema for user properties.
|
|
19
|
+
* @returns A request handler for plan updates.
|
|
20
|
+
*/
|
|
21
|
+
export declare const updatePlan: ({ name, tableName, schema: _schema }: types.Resource) => (req: UpdatePlanRequest) => Promise<{
|
|
22
|
+
statusCode: number;
|
|
23
|
+
body: {
|
|
24
|
+
error: string;
|
|
25
|
+
errorKey: string;
|
|
26
|
+
props?: undefined;
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
statusCode: number;
|
|
30
|
+
body: {
|
|
31
|
+
props: Pick<import("@molecule/api-resource/types").Props, "updatedAt"> & Record<string, unknown>;
|
|
32
|
+
error?: undefined;
|
|
33
|
+
errorKey?: undefined;
|
|
34
|
+
};
|
|
35
|
+
} | {
|
|
36
|
+
statusCode: number;
|
|
37
|
+
body: {
|
|
38
|
+
checkoutUrl: string;
|
|
39
|
+
};
|
|
40
|
+
}>;
|
|
41
|
+
//# sourceMappingURL=updatePlan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updatePlan.d.ts","sourceRoot":"","sources":["../../src/handlers/updatePlan.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAI7D,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAMzC,oEAAoE;AACpE,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,IAAI,EAAE;QACJ,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,CAAA;CACF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,GAAI,sCAAsC,KAAK,CAAC,QAAQ,MAO/D,KAAK,iBAAiB;;;;;aAAL,CAAC;;;;;;;;;;;;;;EAuOjC,CAAA"}
|