@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,242 @@
|
|
|
1
|
+
import { get, getAnalytics, getLogger } from '@molecule/api-bond';
|
|
2
|
+
import { findById } from '@molecule/api-database';
|
|
3
|
+
import { t } from '@molecule/api-i18n';
|
|
4
|
+
import { isConfigNotConfiguredError, } from '@molecule/api-payments';
|
|
5
|
+
import { update as resourceUpdate } from '@molecule/api-resource';
|
|
6
|
+
import { updatePlanPropsSchema } from '../schema.js';
|
|
7
|
+
import { invalidateEntitlementsCacheSafe } from '../utilities/invalidateEntitlements.js';
|
|
8
|
+
const analytics = getAnalytics();
|
|
9
|
+
const logger = getLogger();
|
|
10
|
+
/**
|
|
11
|
+
* Updates a user's subscription plan. Uses the bonded PlanService to look up plan metadata and
|
|
12
|
+
* delegates to the appropriate bonded PaymentProvider based on the plan's `platformKey`. Handles
|
|
13
|
+
* cross-platform subscription cancellation when switching providers. Returns a checkout URL
|
|
14
|
+
* (status 201) for new subscriptions, or directly updates the user record for in-place changes
|
|
15
|
+
* and free plan downgrades.
|
|
16
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
17
|
+
* @param resource.name - The resource name.
|
|
18
|
+
* @param resource.tableName - The database table name for users.
|
|
19
|
+
* @param resource.schema - The validation schema for user properties.
|
|
20
|
+
* @returns A request handler for plan updates.
|
|
21
|
+
*/
|
|
22
|
+
export const updatePlan = ({ name, tableName, schema: _schema }) => {
|
|
23
|
+
const updateResource = resourceUpdate({
|
|
24
|
+
name,
|
|
25
|
+
tableName,
|
|
26
|
+
schema: updatePlanPropsSchema,
|
|
27
|
+
});
|
|
28
|
+
return async (req) => {
|
|
29
|
+
try {
|
|
30
|
+
const id = req.params.id;
|
|
31
|
+
const { planKey } = req.body;
|
|
32
|
+
if (!planKey && planKey !== '') {
|
|
33
|
+
return {
|
|
34
|
+
statusCode: 400,
|
|
35
|
+
body: { error: t('user.error.planKeyRequired'), errorKey: 'user.error.planKeyRequired' },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Get the user.
|
|
39
|
+
const user = await findById(tableName, id);
|
|
40
|
+
if (!user) {
|
|
41
|
+
return {
|
|
42
|
+
statusCode: 404,
|
|
43
|
+
body: { error: t('user.error.notFound'), errorKey: 'user.error.notFound' },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Try to get plans from bond.
|
|
47
|
+
const plans = get('plans');
|
|
48
|
+
if (plans) {
|
|
49
|
+
const plan = plans.findPlan(planKey);
|
|
50
|
+
const previousPlan = plans.findPlan(user.planKey || '');
|
|
51
|
+
if (!plan) {
|
|
52
|
+
return {
|
|
53
|
+
statusCode: 400,
|
|
54
|
+
body: { error: t('user.error.invalidPlan'), errorKey: 'user.error.invalidPlan' },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// If switching away from a different platform, cancel the previous subscription.
|
|
58
|
+
if (previousPlan &&
|
|
59
|
+
previousPlan.platformKey &&
|
|
60
|
+
previousPlan.platformKey !== plan.platformKey) {
|
|
61
|
+
const previousProvider = get('payments', previousPlan.platformKey);
|
|
62
|
+
if (previousProvider?.cancelSubscription) {
|
|
63
|
+
await previousProvider.cancelSubscription({ userId: user.id });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Delegate to platform-specific payment provider if available.
|
|
67
|
+
if (plan.platformKey) {
|
|
68
|
+
const paymentProvider = get('payments', plan.platformKey);
|
|
69
|
+
if (paymentProvider?.updateSubscription) {
|
|
70
|
+
const result = await paymentProvider.updateSubscription({
|
|
71
|
+
userId: user.id,
|
|
72
|
+
newProductId: plan.platformProductId,
|
|
73
|
+
previousProductId: previousPlan?.platformProductId,
|
|
74
|
+
});
|
|
75
|
+
if (result.checkoutUrl) {
|
|
76
|
+
// New subscription requires checkout — return the URL to the client.
|
|
77
|
+
analytics
|
|
78
|
+
.track({
|
|
79
|
+
name: 'user.plan_checkout_created',
|
|
80
|
+
userId: id,
|
|
81
|
+
properties: {
|
|
82
|
+
previousPlanKey: previousPlan?.planKey,
|
|
83
|
+
newPlanKey: plan.planKey,
|
|
84
|
+
platformKey: plan.platformKey,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
.catch(() => { });
|
|
88
|
+
return { statusCode: 201, body: { checkoutUrl: result.checkoutUrl } };
|
|
89
|
+
}
|
|
90
|
+
if (result.updated) {
|
|
91
|
+
// Subscription was updated in-place — update the user record.
|
|
92
|
+
analytics
|
|
93
|
+
.track({
|
|
94
|
+
name: 'user.plan_updated',
|
|
95
|
+
userId: id,
|
|
96
|
+
properties: {
|
|
97
|
+
previousPlanKey: previousPlan?.planKey,
|
|
98
|
+
newPlanKey: plan.planKey,
|
|
99
|
+
platformKey: plan.platformKey,
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
.catch(() => { });
|
|
103
|
+
const updated = await updateResource({
|
|
104
|
+
id,
|
|
105
|
+
props: {
|
|
106
|
+
planKey: plan.planKey,
|
|
107
|
+
planExpiresAt: result.subscription?.expiresAt,
|
|
108
|
+
planAutoRenews: result.subscription?.autoRenews,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
// Drop the cached plan key so the plan change takes effect
|
|
112
|
+
// immediately, not after the entitlements cache TTL.
|
|
113
|
+
invalidateEntitlementsCacheSafe(id);
|
|
114
|
+
return updated;
|
|
115
|
+
}
|
|
116
|
+
analytics
|
|
117
|
+
.track({
|
|
118
|
+
name: 'user.plan_update_failed',
|
|
119
|
+
userId: id,
|
|
120
|
+
properties: {
|
|
121
|
+
previousPlanKey: previousPlan?.planKey,
|
|
122
|
+
newPlanKey: plan.planKey,
|
|
123
|
+
platformKey: plan.platformKey,
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
.catch(() => { });
|
|
127
|
+
return {
|
|
128
|
+
statusCode: 400,
|
|
129
|
+
body: {
|
|
130
|
+
error: t('user.error.failedToUpdateSubscription'),
|
|
131
|
+
errorKey: 'user.error.failedToUpdateSubscription',
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// The plan is tied to a payment platform, but the bonded provider has
|
|
136
|
+
// no in-place `updateSubscription` (e.g. Apple/Google receipt-only
|
|
137
|
+
// providers). NEVER direct-set a paid planKey without a positive
|
|
138
|
+
// provider confirmation — activation MUST go through receipt
|
|
139
|
+
// verification (POST /users/:id/verify-payment/:provider). Falling
|
|
140
|
+
// through here previously let a user self-grant a paid (Pro) plan with
|
|
141
|
+
// no payment.
|
|
142
|
+
return {
|
|
143
|
+
statusCode: 400,
|
|
144
|
+
body: {
|
|
145
|
+
error: t('user.error.subscriptionActivationRequiresVerification', undefined, {
|
|
146
|
+
defaultValue: 'Subscription activation must go through receipt verification — use POST /users/:id/verify-payment/:provider.',
|
|
147
|
+
}),
|
|
148
|
+
errorKey: 'user.error.subscriptionActivationRequiresVerification',
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// No platform handler — free-plan downgrade only.
|
|
153
|
+
if (plan.planKey === '') {
|
|
154
|
+
// Switching to free plan. Keep current planKey if subscription hasn't expired yet.
|
|
155
|
+
const now = new Date().getTime();
|
|
156
|
+
const expires = new Date(user.planExpiresAt || 0).getTime();
|
|
157
|
+
analytics
|
|
158
|
+
.track({
|
|
159
|
+
name: 'user.plan_updated',
|
|
160
|
+
userId: id,
|
|
161
|
+
properties: { previousPlanKey: previousPlan?.planKey, newPlanKey: '' },
|
|
162
|
+
})
|
|
163
|
+
.catch(() => { });
|
|
164
|
+
const downgraded = await updateResource({
|
|
165
|
+
id,
|
|
166
|
+
props: {
|
|
167
|
+
planKey: now > expires ? plan.planKey : (user.planKey ?? plan.planKey),
|
|
168
|
+
planAutoRenews: false,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
// Drop the cached plan key so the downgrade takes effect
|
|
172
|
+
// immediately, not after the entitlements cache TTL.
|
|
173
|
+
invalidateEntitlementsCacheSafe(id);
|
|
174
|
+
return downgraded;
|
|
175
|
+
}
|
|
176
|
+
// A non-free plan with NO payment platform handler cannot be confirmed
|
|
177
|
+
// by any provider — refuse to grant it. Only the free plan ('') may be
|
|
178
|
+
// written without provider confirmation (handled above); every paid
|
|
179
|
+
// planKey must go through a provider `updateSubscription`
|
|
180
|
+
// (checkout/in-place) or receipt verification
|
|
181
|
+
// (POST /users/:id/verify-payment/:provider). This closes the
|
|
182
|
+
// self-grant escalation where a user PATCHed a paid planKey directly.
|
|
183
|
+
return {
|
|
184
|
+
statusCode: 400,
|
|
185
|
+
body: {
|
|
186
|
+
error: t('user.error.subscriptionActivationRequiresVerification', undefined, {
|
|
187
|
+
defaultValue: 'Subscription activation must go through receipt verification — use POST /users/:id/verify-payment/:provider.',
|
|
188
|
+
}),
|
|
189
|
+
errorKey: 'user.error.subscriptionActivationRequiresVerification',
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// No plans service bonded — only allow downgrade to free plan.
|
|
194
|
+
// Upgrades MUST go through a bonded PlanService + PaymentProvider to prevent
|
|
195
|
+
// privilege escalation (users setting planKey to a paid tier without payment).
|
|
196
|
+
if (planKey !== '') {
|
|
197
|
+
return {
|
|
198
|
+
statusCode: 400,
|
|
199
|
+
body: {
|
|
200
|
+
error: t('user.error.failedToUpdatePlan'),
|
|
201
|
+
errorKey: 'user.error.failedToUpdatePlan',
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
analytics
|
|
206
|
+
.track({
|
|
207
|
+
name: 'user.plan_updated',
|
|
208
|
+
userId: id,
|
|
209
|
+
properties: { newPlanKey: planKey },
|
|
210
|
+
})
|
|
211
|
+
.catch(() => { });
|
|
212
|
+
const result = await updateResource({ id, props: { planKey, planAutoRenews: false } });
|
|
213
|
+
// Drop the cached plan key so the downgrade takes effect immediately,
|
|
214
|
+
// not after the entitlements cache TTL.
|
|
215
|
+
invalidateEntitlementsCacheSafe(id);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// A bonded payment provider rethrows a tagged config-not-configured
|
|
220
|
+
// error (e.g. STRIPE_SECRET_KEY unset) instead of swallowing it — pass
|
|
221
|
+
// its REAL statusCode/errorKey through instead of flattening it to a
|
|
222
|
+
// generic 500. Otherwise a missing secret is indistinguishable from an
|
|
223
|
+
// unexpected server error at the exact moment a user tries to upgrade.
|
|
224
|
+
if (isConfigNotConfiguredError(error)) {
|
|
225
|
+
logger.warn(error.message, { errorKey: error.errorKey });
|
|
226
|
+
return {
|
|
227
|
+
statusCode: error.statusCode,
|
|
228
|
+
body: { error: error.message, errorKey: error.errorKey },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
logger.error(error);
|
|
232
|
+
return {
|
|
233
|
+
statusCode: 500,
|
|
234
|
+
body: {
|
|
235
|
+
error: t('user.error.failedToUpdatePlan'),
|
|
236
|
+
errorKey: 'user.error.failedToUpdatePlan',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
//# sourceMappingURL=updatePlan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updatePlan.js","sourceRoot":"","sources":["../../src/handlers/updatePlan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { MoleculeRequest } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/** Request body for two-factor authentication operations (setup, enable, or disable). */
|
|
4
|
+
export interface VerifyTwoFactorRequest extends MoleculeRequest {
|
|
5
|
+
body: {
|
|
6
|
+
token?: string;
|
|
7
|
+
action?: 'setup' | 'enable' | 'disable';
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Handles two-factor authentication lifecycle via `@molecule/api-two-factor`:
|
|
12
|
+
* - `setup`: Generates a TOTP secret and returns QR code URLs for authenticator apps.
|
|
13
|
+
* - `enable`: Verifies a TOTP token against the pending secret, then activates 2FA.
|
|
14
|
+
* - `disable`: Verifies a TOTP token against the active secret, then deactivates 2FA.
|
|
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 two-factor authentication operations.
|
|
20
|
+
*/
|
|
21
|
+
export declare const verifyTwoFactor: ({ name: _name, tableName, schema: _schema }: types.Resource) => (req: VerifyTwoFactorRequest) => Promise<{
|
|
22
|
+
statusCode: number;
|
|
23
|
+
body: {
|
|
24
|
+
error: string;
|
|
25
|
+
errorKey: string;
|
|
26
|
+
twoFactorEnabled?: undefined;
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
statusCode: number;
|
|
30
|
+
body: {
|
|
31
|
+
secret: string;
|
|
32
|
+
keyUrl: string;
|
|
33
|
+
QRImageUrl: string;
|
|
34
|
+
error?: undefined;
|
|
35
|
+
errorKey?: undefined;
|
|
36
|
+
twoFactorEnabled?: undefined;
|
|
37
|
+
};
|
|
38
|
+
} | {
|
|
39
|
+
statusCode: number;
|
|
40
|
+
body: {
|
|
41
|
+
twoFactorEnabled: boolean;
|
|
42
|
+
error?: undefined;
|
|
43
|
+
errorKey?: undefined;
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
//# sourceMappingURL=verifyTwoFactor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifyTwoFactor.d.ts","sourceRoot":"","sources":["../../src/handlers/verifyTwoFactor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAKzC,yFAAyF;AACzF,MAAM,WAAW,sBAAuB,SAAQ,eAAe;IAC7D,IAAI,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAA;KACxC,CAAA;CACF;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAAI,6CAA6C,KAAK,CAAC,QAAQ,MAC3E,KAAK,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;EAsK1C,CAAA"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { getAnalytics, getLogger } from '@molecule/api-bond';
|
|
2
|
+
import { get } from '@molecule/api-config';
|
|
3
|
+
import { findById, updateById } from '@molecule/api-database';
|
|
4
|
+
import { t } from '@molecule/api-i18n';
|
|
5
|
+
const analytics = getAnalytics();
|
|
6
|
+
const logger = getLogger();
|
|
7
|
+
/**
|
|
8
|
+
* Handles two-factor authentication lifecycle via `@molecule/api-two-factor`:
|
|
9
|
+
* - `setup`: Generates a TOTP secret and returns QR code URLs for authenticator apps.
|
|
10
|
+
* - `enable`: Verifies a TOTP token against the pending secret, then activates 2FA.
|
|
11
|
+
* - `disable`: Verifies a TOTP token against the active secret, then deactivates 2FA.
|
|
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 for two-factor authentication operations.
|
|
17
|
+
*/
|
|
18
|
+
export const verifyTwoFactor = ({ name: _name, tableName, schema: _schema }) => {
|
|
19
|
+
return async (req) => {
|
|
20
|
+
try {
|
|
21
|
+
const id = req.params.id;
|
|
22
|
+
const { token, action } = req.body;
|
|
23
|
+
// Get user secrets.
|
|
24
|
+
const secrets = await findById(`${tableName}Secrets`, id);
|
|
25
|
+
if (!secrets) {
|
|
26
|
+
return {
|
|
27
|
+
statusCode: 404,
|
|
28
|
+
body: { error: t('user.error.notFound'), errorKey: 'user.error.notFound' },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const twoFactor = await import('@molecule/api-two-factor').catch(() => null);
|
|
32
|
+
if (!twoFactor) {
|
|
33
|
+
return {
|
|
34
|
+
statusCode: 500,
|
|
35
|
+
body: {
|
|
36
|
+
error: t('user.error.twoFactorNotAvailable'),
|
|
37
|
+
errorKey: 'user.error.twoFactorNotAvailable',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (action === 'setup') {
|
|
42
|
+
// Generate a new secret and return QR code.
|
|
43
|
+
const secret = twoFactor.generateSecret();
|
|
44
|
+
// Store as pending secret.
|
|
45
|
+
await updateById(`${tableName}Secrets`, id, { pendingTwoFactorSecret: secret });
|
|
46
|
+
const appName = get('APP_NAME', 'App') ?? 'App';
|
|
47
|
+
const user = await findById(tableName, id);
|
|
48
|
+
const urls = await twoFactor.getUrls({
|
|
49
|
+
username: user?.username || id,
|
|
50
|
+
service: appName,
|
|
51
|
+
secret,
|
|
52
|
+
});
|
|
53
|
+
return { statusCode: 200, body: { ...urls, secret } };
|
|
54
|
+
}
|
|
55
|
+
if (action === 'enable') {
|
|
56
|
+
if (!token) {
|
|
57
|
+
return {
|
|
58
|
+
statusCode: 400,
|
|
59
|
+
body: { error: t('user.error.tokenRequired'), errorKey: 'user.error.tokenRequired' },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!secrets.pendingTwoFactorSecret) {
|
|
63
|
+
return {
|
|
64
|
+
statusCode: 400,
|
|
65
|
+
body: {
|
|
66
|
+
error: t('user.error.noPendingTwoFactorSetup'),
|
|
67
|
+
errorKey: 'user.error.noPendingTwoFactorSetup',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Verify the token against the pending secret.
|
|
72
|
+
const verifyResult = await twoFactor.verify({
|
|
73
|
+
secret: secrets.pendingTwoFactorSecret,
|
|
74
|
+
token,
|
|
75
|
+
// Reject a code whose time step was already consumed (replay protection).
|
|
76
|
+
afterTimeStep: secrets.lastTwoFactorTimeStep,
|
|
77
|
+
});
|
|
78
|
+
if (!verifyResult.valid) {
|
|
79
|
+
return {
|
|
80
|
+
statusCode: 403,
|
|
81
|
+
body: { error: t('user.error.invalidToken'), errorKey: 'user.error.invalidToken' },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Move pending secret to active and enable 2FA. Persist the consumed
|
|
85
|
+
// time step so the enabling code can't be replayed (single-use).
|
|
86
|
+
await updateById(`${tableName}Secrets`, id, {
|
|
87
|
+
twoFactorSecret: secrets.pendingTwoFactorSecret,
|
|
88
|
+
pendingTwoFactorSecret: null,
|
|
89
|
+
...(verifyResult.timeStep !== undefined
|
|
90
|
+
? { lastTwoFactorTimeStep: verifyResult.timeStep }
|
|
91
|
+
: {}),
|
|
92
|
+
});
|
|
93
|
+
await updateById(tableName, id, {
|
|
94
|
+
twoFactorEnabled: true,
|
|
95
|
+
updatedAt: new Date().toISOString(),
|
|
96
|
+
});
|
|
97
|
+
analytics.track({ name: 'user.two_factor_enabled', userId: id }).catch(() => { });
|
|
98
|
+
return { statusCode: 200, body: { twoFactorEnabled: true } };
|
|
99
|
+
}
|
|
100
|
+
if (action === 'disable') {
|
|
101
|
+
if (!token) {
|
|
102
|
+
return {
|
|
103
|
+
statusCode: 400,
|
|
104
|
+
body: { error: t('user.error.tokenRequired'), errorKey: 'user.error.tokenRequired' },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (!secrets.twoFactorSecret) {
|
|
108
|
+
return {
|
|
109
|
+
statusCode: 400,
|
|
110
|
+
body: {
|
|
111
|
+
error: t('user.error.twoFactorNotEnabled'),
|
|
112
|
+
errorKey: 'user.error.twoFactorNotEnabled',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Verify the token against the current secret.
|
|
117
|
+
const verifyResult = await twoFactor.verify({
|
|
118
|
+
secret: secrets.twoFactorSecret,
|
|
119
|
+
token,
|
|
120
|
+
// Reject a code whose time step was already consumed (replay protection).
|
|
121
|
+
afterTimeStep: secrets.lastTwoFactorTimeStep,
|
|
122
|
+
});
|
|
123
|
+
if (!verifyResult.valid) {
|
|
124
|
+
return {
|
|
125
|
+
statusCode: 403,
|
|
126
|
+
body: { error: t('user.error.invalidToken'), errorKey: 'user.error.invalidToken' },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Disable 2FA. Persist the consumed time step so the disabling code
|
|
130
|
+
// can't be replayed (single-use) within its validity window.
|
|
131
|
+
await updateById(`${tableName}Secrets`, id, {
|
|
132
|
+
twoFactorSecret: null,
|
|
133
|
+
pendingTwoFactorSecret: null,
|
|
134
|
+
...(verifyResult.timeStep !== undefined
|
|
135
|
+
? { lastTwoFactorTimeStep: verifyResult.timeStep }
|
|
136
|
+
: {}),
|
|
137
|
+
});
|
|
138
|
+
await updateById(tableName, id, {
|
|
139
|
+
twoFactorEnabled: false,
|
|
140
|
+
updatedAt: new Date().toISOString(),
|
|
141
|
+
});
|
|
142
|
+
analytics.track({ name: 'user.two_factor_disabled', userId: id }).catch(() => { });
|
|
143
|
+
return { statusCode: 200, body: { twoFactorEnabled: false } };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
statusCode: 400,
|
|
147
|
+
body: { error: t('user.error.invalidAction'), errorKey: 'user.error.invalidAction' },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
logger.error(error);
|
|
152
|
+
return {
|
|
153
|
+
statusCode: 500,
|
|
154
|
+
body: {
|
|
155
|
+
error: t('user.error.twoFactorOperationFailed'),
|
|
156
|
+
errorKey: 'user.error.twoFactorOperationFailed',
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=verifyTwoFactor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifyTwoFactor.js","sourceRoot":"","sources":["../../src/handlers/verifyTwoFactor.ts"],"names":[],"mappings":"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"}
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkBH;;GAEG;AACH,eAAO,MAAM,cAAc,OAAO,CAAA"}
|
package/dist/i18n.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale registration for the user resource.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { registerLocaleModule } from '@molecule/api-i18n';
|
|
7
|
+
try {
|
|
8
|
+
const userLocales = await import('@molecule/api-locales-user');
|
|
9
|
+
registerLocaleModule(userLocales);
|
|
10
|
+
}
|
|
11
|
+
catch (_error) {
|
|
12
|
+
// Locale package not available (not built or not installed).
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const paymentLocales = await import('@molecule/api-locales-user-payments');
|
|
16
|
+
registerLocaleModule(paymentLocales);
|
|
17
|
+
}
|
|
18
|
+
catch (_error) {
|
|
19
|
+
// Locale package not available (not built or not installed).
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The i18n registered.
|
|
23
|
+
*/
|
|
24
|
+
export const i18nRegistered = true;
|
|
25
|
+
//# sourceMappingURL=i18n.js.map
|
package/dist/i18n.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAEzD,IAAI,CAAC;IACH,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,4BAA4B,CAAC,CAAA;IAC9D,oBAAoB,CAAC,WAAW,CAAC,CAAA;AACnC,CAAC;AAAC,OAAO,MAAM,EAAE,CAAC;IAChB,6DAA6D;AAC/D,CAAC;AAED,IAAI,CAAC;IACH,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,qCAAqC,CAAC,CAAA;IAC1E,oBAAoB,CAAC,cAAc,CAAC,CAAA;AACtC,CAAC;AAAC,OAAO,MAAM,EAAE,CAAC;IAChB,6DAA6D;AAC/D,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `user` resource types, schema, and definition.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* The user record is split across TWO schemas — pick the right one or you leak credentials:
|
|
6
|
+
* - **{@link Props} (`propsSchema`)** — SAFE, client-facing fields (username, name, email,
|
|
7
|
+
* `emailVerified`, `twoFactorEnabled`, plan). This is what handlers return and what lives
|
|
8
|
+
* in the `users` table.
|
|
9
|
+
* - **{@link SecretProps} (`secretPropsSchema`)** — SERVER-ONLY secrets: `passwordHash`, the
|
|
10
|
+
* TOTP `twoFactorSecret` (and its pending-setup value). Stored in a SEPARATE secrets table
|
|
11
|
+
* and NEVER serialized to the client. Note the pair `twoFactorEnabled` (safe boolean, in
|
|
12
|
+
* `Props`) vs `twoFactorSecret` (secret, in `SecretProps`).
|
|
13
|
+
*
|
|
14
|
+
* When you extend the user, put a secret (token, hash, key, provider refresh token) in
|
|
15
|
+
* `SecretProps`; put a display field in `Props`. **Never add a secret to `Props`, never
|
|
16
|
+
* return a secrets-table value in a response or log, and never `res.json(userRow)` a raw DB
|
|
17
|
+
* row** — return `Props`.
|
|
18
|
+
*
|
|
19
|
+
* Auth is ALREADY wired globally (the router's `verifyMiddleware` → `res.locals.session`),
|
|
20
|
+
* so a handler reads the current user with `getUserId(res)` and does NOT add per-route auth
|
|
21
|
+
* middleware (see the `auth` skill). Scope every custom user query by the authenticated id.
|
|
22
|
+
*
|
|
23
|
+
* On the CLIENT, the bearer token is held IN MEMORY only — a `localStorage` copy is
|
|
24
|
+
* XSS-exfiltratable and is forbidden. The session is restored after a reload via the
|
|
25
|
+
* httpOnly cookie + `GET /users/me`; don't persist the token yourself.
|
|
26
|
+
*
|
|
27
|
+
* **Client-facing endpoints** (mounted under the app's `/api` prefix → `/api/users/...`).
|
|
28
|
+
* The auth CLIENT (`useAuth()` → `login` / `register` / `logout` / `refresh`) already wraps
|
|
29
|
+
* login / signup / logout — do NOT hand-roll those against the raw routes. The rest have NO
|
|
30
|
+
* client method; call them with raw `http.*`. Use these EXACT paths — a weak model guesses
|
|
31
|
+
* `/api/auth/*` or `/api/user` (singular), and neither exists:
|
|
32
|
+
* - `POST /api/users/forgot-password` — request a reset email (body `{ email }`)
|
|
33
|
+
* - `POST /api/users/reset-password` — confirm with the emailed token (body `{ token, password }`)
|
|
34
|
+
* - `PATCH /api/users/:id` — update profile fields (name, username, email, bio); NOT `PUT /api/user`
|
|
35
|
+
* - `PATCH /api/users/:id/password` — change password · `DELETE /api/users/:id` — delete account
|
|
36
|
+
* - `PATCH /api/users/:id/plan` — update the subscription plan
|
|
37
|
+
* - `GET /api/users/me` — the current user (session restore) · `GET /api/users/:id` — read one
|
|
38
|
+
* The full, authoritative route list is the `routes` export (see `routes.ts`).
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* // Extend safely: a display field in Props, a secret in SecretProps.
|
|
43
|
+
* // propsSchema: { …, timezone: z.string().optional() } // safe → client
|
|
44
|
+
* // secretPropsSchema: { …, passwordResetToken: z.string().optional() } // server-only, secrets table
|
|
45
|
+
*
|
|
46
|
+
* // A custom handler returns SAFE props — never the secrets row.
|
|
47
|
+
* router.get('/me/timezone', async (req, res) => {
|
|
48
|
+
* const userId = getUserId(res)
|
|
49
|
+
* if (!userId) return res.status(401).json({ error: 'Authentication required.' })
|
|
50
|
+
* const user = await findById('users', userId) // the users table holds Props only
|
|
51
|
+
* res.json({ timezone: user?.timezone }) // never spread a secrets-table row here
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @e2e
|
|
56
|
+
* Integration checklist — drive the real UI (live preview, no mocks), adapt
|
|
57
|
+
* each item to this app's actual screens/flows, and check every box off one
|
|
58
|
+
* by one. A box you can't check is an integration bug to fix — not a skip:
|
|
59
|
+
* - [ ] A new user can sign up with email + password and lands authenticated (the
|
|
60
|
+
* UI reflects the signed-in user, e.g. their name/menu appears).
|
|
61
|
+
* - [ ] Any flow that emails a link/code (signup verification, password reset)
|
|
62
|
+
* round-trips: the sandbox CAPTURES the message instead of sending — read it
|
|
63
|
+
* with the `read_activity` tool (filter type 'email') and follow the link/code
|
|
64
|
+
* in its payload; never mock the flow or modify production code to expose it.
|
|
65
|
+
* - [ ] Logging out and logging back in with the same credentials reaches the same
|
|
66
|
+
* account and its data.
|
|
67
|
+
* - [ ] The session survives a full page reload (restored via the httpOnly cookie +
|
|
68
|
+
* `GET /users/me` — never from a token persisted in localStorage).
|
|
69
|
+
* - [ ] A wrong password shows a visible error and does NOT authenticate.
|
|
70
|
+
* - [ ] Authenticated-only screens are unreachable when logged out (redirect to
|
|
71
|
+
* login or an explicit denial — never a blank page).
|
|
72
|
+
* - [ ] A profile/account edit (e.g. display name) persists across a reload.
|
|
73
|
+
*
|
|
74
|
+
* @module
|
|
75
|
+
*/
|
|
76
|
+
export * as authorization from './authorization.js';
|
|
77
|
+
export * as authorizers from './authorizers/index.js';
|
|
78
|
+
export * from './browser-guard.js';
|
|
79
|
+
export * as handlers from './handlers/index.js';
|
|
80
|
+
export * from './i18n.js';
|
|
81
|
+
export * from './requestHandlerMap.js';
|
|
82
|
+
export * from './resource.js';
|
|
83
|
+
export * from './routes.js';
|
|
84
|
+
export * from './schema.js';
|
|
85
|
+
export * from './secrets.js';
|
|
86
|
+
export * as types from './types.js';
|
|
87
|
+
export * as utilities from './utilities/index.js';
|
|
88
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AAEH,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,WAAW,MAAM,wBAAwB,CAAA;AACrD,cAAc,oBAAoB,CAAA;AAClC,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAA;AAC/C,cAAc,WAAW,CAAA;AACzB,cAAc,wBAAwB,CAAA;AACtC,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAA"}
|