@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,383 @@
|
|
|
1
|
+
import { get, getAnalytics, getLogger, require as bondRequire } from '@molecule/api-bond';
|
|
2
|
+
import { t } from '@molecule/api-i18n';
|
|
3
|
+
import { isConfigNotConfiguredError, } from '@molecule/api-payments';
|
|
4
|
+
import { update as resourceUpdate } from '@molecule/api-resource';
|
|
5
|
+
import { updatePlanPropsSchema } from '../../schema.js';
|
|
6
|
+
import { invalidateEntitlementsCacheSafe } from '../../utilities/invalidateEntitlements.js';
|
|
7
|
+
const analytics = getAnalytics();
|
|
8
|
+
const logger = getLogger();
|
|
9
|
+
/**
|
|
10
|
+
* Generic payment verification handler that works with any bonded PaymentProvider. Reads the
|
|
11
|
+
* provider name from the `:provider` route parameter and dispatches based on `verifyFlow`:
|
|
12
|
+
* - `'subscription'` (Stripe-style): Calls `verifySubscription(subscriptionId)`.
|
|
13
|
+
* - Default (Apple/Google-style): Calls `verifyReceipt()` or `verifyPurchase()` with a receipt
|
|
14
|
+
* string and the plan's platform product ID.
|
|
15
|
+
*
|
|
16
|
+
* On successful verification, updates the user's plan (planKey, planExpiresAt, planAutoRenews)
|
|
17
|
+
* and stores the payment record via the bonded PaymentRecordService.
|
|
18
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
19
|
+
* @param resource.name - The resource name.
|
|
20
|
+
* @param resource.tableName - The database table name for users.
|
|
21
|
+
* @param resource.schema - The validation schema for user properties.
|
|
22
|
+
* @returns A request handler for payment verification.
|
|
23
|
+
*/
|
|
24
|
+
export const verifyPayment = ({ name, tableName, schema: _schema }) => {
|
|
25
|
+
return async (req) => {
|
|
26
|
+
const providerName = req.params.provider;
|
|
27
|
+
if (!providerName) {
|
|
28
|
+
return {
|
|
29
|
+
statusCode: 400,
|
|
30
|
+
body: {
|
|
31
|
+
error: t('user.payment.providerRequired'),
|
|
32
|
+
errorKey: 'user.payment.providerRequired',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const provider = bondRequire('payments', providerName);
|
|
38
|
+
const plans = bondRequire('plans');
|
|
39
|
+
let verified = null;
|
|
40
|
+
let receipt;
|
|
41
|
+
// Dispatch based on verifyFlow (with method-existence fallback).
|
|
42
|
+
const isSubscriptionFlow = provider.verifyFlow === 'subscription' ||
|
|
43
|
+
(!provider.verifyFlow && !!provider.verifySubscription);
|
|
44
|
+
if (isSubscriptionFlow) {
|
|
45
|
+
// Stripe-style: subscriptionId in body or query. Providers that redirect
|
|
46
|
+
// back to this endpoint append their OWN parameter names — PayPal adds
|
|
47
|
+
// `subscription_id` (billing subscriptions) or `token` (v2 orders) to the
|
|
48
|
+
// return_url query — so accept those snake_case echoes as fallbacks. The
|
|
49
|
+
// value is still verified with the provider below before anything grants.
|
|
50
|
+
const subscriptionId = (req.body?.subscriptionId ??
|
|
51
|
+
req.query?.subscriptionId ??
|
|
52
|
+
req.query?.subscription_id ??
|
|
53
|
+
req.query?.token);
|
|
54
|
+
if (!subscriptionId) {
|
|
55
|
+
return {
|
|
56
|
+
statusCode: 400,
|
|
57
|
+
body: {
|
|
58
|
+
error: t('user.payment.subscriptionIdRequired', { provider: providerName }),
|
|
59
|
+
errorKey: 'user.payment.subscriptionIdRequired',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (!provider.verifySubscription) {
|
|
64
|
+
return {
|
|
65
|
+
statusCode: 500,
|
|
66
|
+
body: {
|
|
67
|
+
error: t('user.payment.verificationNotConfigured', { provider: providerName }),
|
|
68
|
+
errorKey: 'user.payment.verificationNotConfigured',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
verified = await provider.verifySubscription(subscriptionId);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Apple/Google-style: receipt + planKey in body.
|
|
76
|
+
const body = req.body || {};
|
|
77
|
+
receipt = body.receipt;
|
|
78
|
+
const planKey = body.planKey;
|
|
79
|
+
if (!receipt || !planKey) {
|
|
80
|
+
return {
|
|
81
|
+
statusCode: 400,
|
|
82
|
+
body: {
|
|
83
|
+
error: t('user.payment.receiptAndPlanRequired', { provider: providerName }),
|
|
84
|
+
errorKey: 'user.payment.receiptAndPlanRequired',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const plan = plans.findPlan(planKey);
|
|
89
|
+
if (!plan || plan.platformKey !== providerName) {
|
|
90
|
+
return {
|
|
91
|
+
statusCode: 400,
|
|
92
|
+
body: {
|
|
93
|
+
error: t('user.payment.invalidPlan', { provider: providerName }),
|
|
94
|
+
errorKey: 'user.payment.invalidPlan',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const verifyFn = provider.verifyReceipt ?? provider.verifyPurchase;
|
|
99
|
+
if (!verifyFn) {
|
|
100
|
+
return {
|
|
101
|
+
statusCode: 500,
|
|
102
|
+
body: {
|
|
103
|
+
error: t('user.payment.verificationNotConfigured', { provider: providerName }),
|
|
104
|
+
errorKey: 'user.payment.verificationNotConfigured',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
verified = await verifyFn.call(provider, receipt, plan.platformProductId);
|
|
109
|
+
}
|
|
110
|
+
if (!verified) {
|
|
111
|
+
const id = req.params.id;
|
|
112
|
+
analytics
|
|
113
|
+
.track({
|
|
114
|
+
name: 'payment.verification_failed',
|
|
115
|
+
userId: id,
|
|
116
|
+
properties: { provider: providerName },
|
|
117
|
+
})
|
|
118
|
+
.catch(() => { });
|
|
119
|
+
return {
|
|
120
|
+
statusCode: 400,
|
|
121
|
+
body: {
|
|
122
|
+
error: t('user.payment.verificationFailed', { provider: providerName }),
|
|
123
|
+
errorKey: 'user.payment.verificationFailed',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Subscription-flow ownership binding. The resolved subscription must
|
|
128
|
+
// belong to THIS user, so a caller cannot upgrade themselves by submitting
|
|
129
|
+
// a subscription id they merely learned (leaked log / shared success_url /
|
|
130
|
+
// support channel):
|
|
131
|
+
// • If the user already has a stored customer id for this provider, the
|
|
132
|
+
// verified subscription's customer MUST match it.
|
|
133
|
+
// • Otherwise (no prior record) a grant is only allowed when the
|
|
134
|
+
// verification came through the per-user, unguessable checkout-session
|
|
135
|
+
// redirect (`viaCheckoutSession`) — never from a bare, caller-supplied
|
|
136
|
+
// subscription id.
|
|
137
|
+
if (isSubscriptionFlow) {
|
|
138
|
+
const ownerId = req.params.id;
|
|
139
|
+
const data = (verified.data ?? {});
|
|
140
|
+
const verifiedCustomerId = data.customerId;
|
|
141
|
+
const records = get('paymentRecords');
|
|
142
|
+
const existing = records ? await records.findByUserId(ownerId, providerName) : null;
|
|
143
|
+
const storedCustomerId = existing?.data?.customerId;
|
|
144
|
+
const ownsByStoredCustomer = !!storedCustomerId && !!verifiedCustomerId && storedCustomerId === verifiedCustomerId;
|
|
145
|
+
const ownsByFreshCheckout = !storedCustomerId && data.viaCheckoutSession === true;
|
|
146
|
+
if (!ownsByStoredCustomer && !ownsByFreshCheckout) {
|
|
147
|
+
analytics
|
|
148
|
+
.track({
|
|
149
|
+
name: 'payment.verification_ownership_rejected',
|
|
150
|
+
userId: ownerId,
|
|
151
|
+
properties: { provider: providerName },
|
|
152
|
+
})
|
|
153
|
+
.catch(() => { });
|
|
154
|
+
return {
|
|
155
|
+
statusCode: 403,
|
|
156
|
+
body: {
|
|
157
|
+
error: t('user.payment.subscriptionOwnershipMismatch', { provider: providerName }, { defaultValue: 'This subscription does not belong to your account.' }),
|
|
158
|
+
errorKey: 'user.payment.subscriptionOwnershipMismatch',
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// First-claim-wins replay defense (M3-1), mirroring the receipt branch.
|
|
163
|
+
// The ownership check above proves the caller MAY claim the subscription;
|
|
164
|
+
// this proves the subscription has not ALREADY been claimed by another
|
|
165
|
+
// account. A `cs_` checkout-session id is reusable by whoever obtained it
|
|
166
|
+
// (e.g. the attacker, from their own legitimate checkout redirect), so
|
|
167
|
+
// without this a single paid session could be replayed to grant premium
|
|
168
|
+
// to unlimited fresh accounts — each would pass `ownsByFreshCheckout`.
|
|
169
|
+
// The atomic, race-safe backstop is the insert-before-grant below; this
|
|
170
|
+
// is the fast, fail-closed pre-check.
|
|
171
|
+
//
|
|
172
|
+
// A subscription with no transaction id cannot be bound to a single
|
|
173
|
+
// account and therefore cannot be protected from replay — fail closed
|
|
174
|
+
// rather than grant an unbounded entitlement.
|
|
175
|
+
if (!verified.transactionId) {
|
|
176
|
+
analytics
|
|
177
|
+
.track({
|
|
178
|
+
name: 'payment.verification_failed',
|
|
179
|
+
userId: ownerId,
|
|
180
|
+
properties: { provider: providerName, reason: 'missing_transaction_id' },
|
|
181
|
+
})
|
|
182
|
+
.catch(() => { });
|
|
183
|
+
return {
|
|
184
|
+
statusCode: 400,
|
|
185
|
+
body: {
|
|
186
|
+
error: t('user.payment.verificationFailed', { provider: providerName }),
|
|
187
|
+
errorKey: 'user.payment.verificationFailed',
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const existingTransaction = records
|
|
192
|
+
? await records.findByTransaction(providerName, verified.transactionId)
|
|
193
|
+
: null;
|
|
194
|
+
if (existingTransaction && existingTransaction.userId !== ownerId) {
|
|
195
|
+
analytics
|
|
196
|
+
.track({
|
|
197
|
+
name: 'payment.verification_ownership_rejected',
|
|
198
|
+
userId: ownerId,
|
|
199
|
+
properties: { provider: providerName },
|
|
200
|
+
})
|
|
201
|
+
.catch(() => { });
|
|
202
|
+
return {
|
|
203
|
+
statusCode: 403,
|
|
204
|
+
body: {
|
|
205
|
+
error: t('user.payment.transactionAlreadyClaimed', { provider: providerName }, { defaultValue: 'This purchase is already associated with another account.' }),
|
|
206
|
+
errorKey: 'user.payment.transactionAlreadyClaimed',
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Receipt-flow (Apple/Google) ownership/replay binding (R2-1), mirroring
|
|
213
|
+
// the subscription block above. A single purchased receipt must bind to
|
|
214
|
+
// exactly ONE account (first-claim-wins) so it cannot be replayed to
|
|
215
|
+
// upgrade many accounts. The atomic, race-safe backstop is the
|
|
216
|
+
// insert-before-grant below; this is the fast, fail-closed pre-check.
|
|
217
|
+
const ownerId = req.params.id;
|
|
218
|
+
// A receipt with no transaction id cannot be bound to a single account
|
|
219
|
+
// and therefore cannot be protected from replay — fail closed rather
|
|
220
|
+
// than grant an unbounded entitlement. (Google `latestOrderId` can be
|
|
221
|
+
// missing.)
|
|
222
|
+
if (!verified.transactionId) {
|
|
223
|
+
analytics
|
|
224
|
+
.track({
|
|
225
|
+
name: 'payment.verification_failed',
|
|
226
|
+
userId: ownerId,
|
|
227
|
+
properties: { provider: providerName, reason: 'missing_transaction_id' },
|
|
228
|
+
})
|
|
229
|
+
.catch(() => { });
|
|
230
|
+
return {
|
|
231
|
+
statusCode: 400,
|
|
232
|
+
body: {
|
|
233
|
+
error: t('user.payment.verificationFailed', { provider: providerName }),
|
|
234
|
+
errorKey: 'user.payment.verificationFailed',
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const records = get('paymentRecords');
|
|
239
|
+
const existing = records
|
|
240
|
+
? await records.findByTransaction(providerName, verified.transactionId)
|
|
241
|
+
: null;
|
|
242
|
+
if (existing && existing.userId !== ownerId) {
|
|
243
|
+
analytics
|
|
244
|
+
.track({
|
|
245
|
+
name: 'payment.verification_ownership_rejected',
|
|
246
|
+
userId: ownerId,
|
|
247
|
+
properties: { provider: providerName },
|
|
248
|
+
})
|
|
249
|
+
.catch(() => { });
|
|
250
|
+
return {
|
|
251
|
+
statusCode: 403,
|
|
252
|
+
body: {
|
|
253
|
+
error: t('user.payment.transactionAlreadyClaimed', { provider: providerName }, { defaultValue: 'This purchase is already associated with another account.' }),
|
|
254
|
+
errorKey: 'user.payment.transactionAlreadyClaimed',
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Find matching plan from the verified platform identifiers. Providers
|
|
260
|
+
// report the parent PRODUCT id on subscriptions, but apps register
|
|
261
|
+
// their catalogue with the PRICE ids checkout was started with — try
|
|
262
|
+
// both before rejecting.
|
|
263
|
+
const plan = plans.findPlanByProductId(verified.productId) ??
|
|
264
|
+
(verified.priceId ? plans.findPlanByProductId(verified.priceId) : null);
|
|
265
|
+
if (!plan) {
|
|
266
|
+
return {
|
|
267
|
+
statusCode: 400,
|
|
268
|
+
body: {
|
|
269
|
+
error: t('user.payment.unknownPlan', { provider: providerName }),
|
|
270
|
+
errorKey: 'user.payment.unknownPlan',
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const id = req.params.id;
|
|
275
|
+
const records = get('paymentRecords');
|
|
276
|
+
// Bind the transaction to THIS user BEFORE granting the plan —
|
|
277
|
+
// fail-closed, first-claim-wins — for BOTH the receipt (Apple/Google, R2-1)
|
|
278
|
+
// and subscription (Stripe, M3-1) flows. The UNIQUE(platformKey,
|
|
279
|
+
// transactionId) index makes a second account's insert fail; `store()`
|
|
280
|
+
// surfaces that conflict (rather than swallowing it) so a replayed receipt
|
|
281
|
+
// OR a replayed checkout-session id is rejected here instead of being
|
|
282
|
+
// granted. Never grant the plan before the transaction is bound, and never
|
|
283
|
+
// swallow the store conflict in a path that has already granted.
|
|
284
|
+
if (records && verified.transactionId) {
|
|
285
|
+
try {
|
|
286
|
+
await records.store({
|
|
287
|
+
userId: id,
|
|
288
|
+
platformKey: providerName,
|
|
289
|
+
transactionId: verified.transactionId,
|
|
290
|
+
productId: verified.productId,
|
|
291
|
+
data: verified.data || {},
|
|
292
|
+
...(receipt ? { receipt } : {}),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
// Conflict: the transaction is already bound. Determine the owner —
|
|
297
|
+
// if it belongs to another account, reject WITHOUT granting; if it is
|
|
298
|
+
// this user re-verifying their own receipt, fall through to an
|
|
299
|
+
// idempotent re-grant.
|
|
300
|
+
const owner = await records.findByTransaction(providerName, verified.transactionId);
|
|
301
|
+
if (!owner || owner.userId !== id) {
|
|
302
|
+
analytics
|
|
303
|
+
.track({
|
|
304
|
+
name: 'payment.verification_ownership_rejected',
|
|
305
|
+
userId: id,
|
|
306
|
+
properties: { provider: providerName },
|
|
307
|
+
})
|
|
308
|
+
.catch(() => { });
|
|
309
|
+
return {
|
|
310
|
+
statusCode: 403,
|
|
311
|
+
body: {
|
|
312
|
+
error: t('user.payment.transactionAlreadyClaimed', { provider: providerName }, { defaultValue: 'This purchase is already associated with another account.' }),
|
|
313
|
+
errorKey: 'user.payment.transactionAlreadyClaimed',
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// Same user re-verifying their own receipt → idempotent re-grant.
|
|
318
|
+
logger.warn('Receipt already recorded for this user; re-granting idempotently', { error });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Update user's plan. Use the partial plan-props schema, NOT the full
|
|
322
|
+
// user `schema` — the resource's full schema makes `id`/`createdAt`/
|
|
323
|
+
// `updatedAt` required and rejects the partial
|
|
324
|
+
// `{ planKey, planExpiresAt, planAutoRenews }` payload applied here
|
|
325
|
+
// (same fix as handlePaymentNotification).
|
|
326
|
+
const updateUser = resourceUpdate({
|
|
327
|
+
name,
|
|
328
|
+
tableName,
|
|
329
|
+
schema: updatePlanPropsSchema,
|
|
330
|
+
});
|
|
331
|
+
const result = await updateUser({
|
|
332
|
+
id,
|
|
333
|
+
props: {
|
|
334
|
+
planKey: plan.planKey,
|
|
335
|
+
planExpiresAt: verified.expiresAt,
|
|
336
|
+
planAutoRenews: verified.autoRenews,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
// The entitlements hot-path caches the plan key (default 5-min TTL) —
|
|
340
|
+
// drop it so the user who just paid sees their new tier immediately.
|
|
341
|
+
invalidateEntitlementsCacheSafe(id);
|
|
342
|
+
analytics
|
|
343
|
+
.track({
|
|
344
|
+
name: 'payment.verified',
|
|
345
|
+
userId: id,
|
|
346
|
+
properties: {
|
|
347
|
+
provider: providerName,
|
|
348
|
+
productId: verified.productId,
|
|
349
|
+
planKey: plan.planKey,
|
|
350
|
+
},
|
|
351
|
+
})
|
|
352
|
+
.catch(() => { });
|
|
353
|
+
// Both flows now bind their payment record BEFORE granting (above), so
|
|
354
|
+
// there is no post-grant store here — a swallowed conflict after a grant
|
|
355
|
+
// is exactly the replay hole this closes (M3-1 / R2-1).
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
// A bonded payment provider rethrows a tagged config-not-configured error
|
|
360
|
+
// (e.g. STRIPE_SECRET_KEY/APPLE_SHARED_SECRET/GOOGLE_API_SERVICE_KEY_OBJECT
|
|
361
|
+
// unset) instead of swallowing it into the same `null` a genuine failed
|
|
362
|
+
// verification returns — pass its REAL statusCode/errorKey through so the
|
|
363
|
+
// actionable "which key, where to get it" message reaches the caller
|
|
364
|
+
// instead of a generic 500 that reads identically to "bad receipt."
|
|
365
|
+
if (isConfigNotConfiguredError(error)) {
|
|
366
|
+
logger.warn(error.message, { errorKey: error.errorKey, provider: providerName });
|
|
367
|
+
return {
|
|
368
|
+
statusCode: error.statusCode,
|
|
369
|
+
body: { error: error.message, errorKey: error.errorKey },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
logger.error(`${providerName} verifyPayment error:`, error);
|
|
373
|
+
return {
|
|
374
|
+
statusCode: 500,
|
|
375
|
+
body: {
|
|
376
|
+
error: t('user.payment.verificationFailed', { provider: providerName }),
|
|
377
|
+
errorKey: 'user.payment.verificationFailed',
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
//# sourceMappingURL=verifyPayment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifyPayment.js","sourceRoot":"","sources":["../../../src/handlers/payments/verifyPayment.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads a user by ID from the database. Attaches plan info (with expiration/renewal status) via the
|
|
5
|
+
* bonded PlanService if available. When reading the authenticated user's own record, also includes
|
|
6
|
+
* two-factor authentication status from the secrets table.
|
|
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 `{ statusCode: 200, body: { props, plan?, twoFactorEnabled? } }`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const read: ({ name: _name, tableName, schema: _schema }: types.Resource) => (req: MoleculeRequest, res: MoleculeResponse) => Promise<{
|
|
14
|
+
statusCode: number;
|
|
15
|
+
body: Record<string, unknown>;
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/handlers/read.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAE/E,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAMzC;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI,GAAI,6CAA6C,KAAK,CAAC,QAAQ,MAChE,KAAK,eAAe,EAAE,KAAK,gBAAgB;;;EA8C1D,CAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getLogger } from '@molecule/api-bond';
|
|
2
|
+
import { findById } from '@molecule/api-database';
|
|
3
|
+
import { t } from '@molecule/api-i18n';
|
|
4
|
+
import { getPlan } from '../utilities/getPlan.js';
|
|
5
|
+
import { stripSensitiveUserColumns } from '../utilities/stripSensitiveUserColumns.js';
|
|
6
|
+
const logger = getLogger();
|
|
7
|
+
/**
|
|
8
|
+
* Reads a user by ID from the database. Attaches plan info (with expiration/renewal status) via the
|
|
9
|
+
* bonded PlanService if available. When reading the authenticated user's own record, also includes
|
|
10
|
+
* two-factor authentication status from the secrets table.
|
|
11
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
12
|
+
* @param resource.name - The resource name.
|
|
13
|
+
* @param resource.tableName - The database table name for users.
|
|
14
|
+
* @param resource.schema - The validation schema for user properties.
|
|
15
|
+
* @returns A request handler that responds with `{ statusCode: 200, body: { props, plan?, twoFactorEnabled? } }`.
|
|
16
|
+
*/
|
|
17
|
+
export const read = ({ name: _name, tableName, schema: _schema }) => {
|
|
18
|
+
return async (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const { session } = res.locals;
|
|
21
|
+
const id = req.params.id;
|
|
22
|
+
const user = await findById(tableName, id);
|
|
23
|
+
if (!user) {
|
|
24
|
+
return {
|
|
25
|
+
statusCode: 404,
|
|
26
|
+
body: { error: t('user.error.notFound'), errorKey: 'user.error.notFound' },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Attach plan info if available.
|
|
30
|
+
const plan = await getPlan(user);
|
|
31
|
+
const body = { props: stripSensitiveUserColumns(user) };
|
|
32
|
+
if (plan) {
|
|
33
|
+
body.plan = plan;
|
|
34
|
+
}
|
|
35
|
+
// If reading self, also include two-factor setup info.
|
|
36
|
+
if (session?.userId === id) {
|
|
37
|
+
try {
|
|
38
|
+
const secrets = await findById(`${tableName}Secrets`, id);
|
|
39
|
+
if (secrets) {
|
|
40
|
+
body.twoFactorEnabled = user.twoFactorEnabled || false;
|
|
41
|
+
body.hasPendingTwoFactor = Boolean(secrets.pendingTwoFactorSecret);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (_error) {
|
|
45
|
+
// Non-critical: the secrets table may not exist yet (pre-migration) or the row may be absent.
|
|
46
|
+
// Omitting twoFactorEnabled/hasPendingTwoFactor from the response is safe — callers treat absence as false.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { statusCode: 200, body };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger.error(error);
|
|
53
|
+
return {
|
|
54
|
+
statusCode: 500,
|
|
55
|
+
body: { error: t('user.error.failedToReadUser'), errorKey: 'user.error.failedToReadUser' },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/handlers/read.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAItC,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACjD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAA;AAErF,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAE1B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAkB,EAAE,EAAE;IAClF,OAAO,KAAK,EAAE,GAAoB,EAAE,GAAqB,EAAE,EAAE;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,MAAM,CAAA;YAC9B,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,EAAY,CAAA;YAElC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAc,SAAS,EAAE,EAAE,CAAC,CAAA;YAEvD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,qBAAqB,CAAC,EAAE,QAAQ,EAAE,qBAAqB,EAAE;iBAC3E,CAAA;YACH,CAAC;YAED,iCAAiC;YACjC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;YAChC,MAAM,IAAI,GAA4B,EAAE,KAAK,EAAE,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAA;YAEhF,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;YAClB,CAAC;YAED,uDAAuD;YACvD,IAAI,OAAO,EAAE,MAAM,KAAK,EAAE,EAAE,CAAC;gBAC3B,IAAI,CAAC;oBACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAoB,GAAG,SAAS,SAAS,EAAE,EAAE,CAAC,CAAA;oBAE5E,IAAI,OAAO,EAAE,CAAC;wBACZ,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,KAAK,CAAA;wBACtD,IAAI,CAAC,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAA;oBACpE,CAAC;gBACH,CAAC;gBAAC,OAAO,MAAM,EAAE,CAAC;oBAChB,8FAA8F;oBAC9F,4GAA4G;gBAC9G,CAAC;YACH,CAAC;YAED,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,6BAA6B,CAAC,EAAE,QAAQ,EAAE,6BAA6B,EAAE;aAC3F,CAAA;QACH,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads the AUTHENTICATED user from the session — no `:id` param. Backs
|
|
5
|
+
* `GET /users/me`, which the app's auth client calls on init to RESTORE the
|
|
6
|
+
* session after a full page load.
|
|
7
|
+
*
|
|
8
|
+
* [M1-1] The bearer token is held in memory (a localStorage copy is
|
|
9
|
+
* XSS-exfiltratable and is forbidden), so it is lost on reload; the httpOnly
|
|
10
|
+
* session cookie is the persistent credential. The `auth` middleware reads that
|
|
11
|
+
* cookie into `res.locals.session`, and this handler returns the current user
|
|
12
|
+
* so the client can re-establish `authenticated` state WITHOUT the token ever
|
|
13
|
+
* being exposed to JavaScript. Always self, so it mirrors `read`'s self branch
|
|
14
|
+
* (plan + two-factor status).
|
|
15
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
16
|
+
* @param resource.tableName - The database table name for users.
|
|
17
|
+
* @returns A request handler responding `{ statusCode: 200, body: { props, plan?, twoFactorEnabled? } }`, or 401 when unauthenticated.
|
|
18
|
+
*/
|
|
19
|
+
export declare const readSelf: ({ tableName }: types.Resource) => (_req: MoleculeRequest, res: MoleculeResponse) => Promise<{
|
|
20
|
+
statusCode: number;
|
|
21
|
+
body: Record<string, unknown>;
|
|
22
|
+
}>;
|
|
23
|
+
//# sourceMappingURL=readSelf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readSelf.d.ts","sourceRoot":"","sources":["../../src/handlers/readSelf.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAE/E,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAMzC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,QAAQ,GAAI,eAAe,KAAK,CAAC,QAAQ,MACtC,MAAM,eAAe,EAAE,KAAK,gBAAgB;;;EAsD3D,CAAA"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getLogger } from '@molecule/api-bond';
|
|
2
|
+
import { findById } from '@molecule/api-database';
|
|
3
|
+
import { t } from '@molecule/api-i18n';
|
|
4
|
+
import { getPlan } from '../utilities/getPlan.js';
|
|
5
|
+
import { stripSensitiveUserColumns } from '../utilities/stripSensitiveUserColumns.js';
|
|
6
|
+
const logger = getLogger();
|
|
7
|
+
/**
|
|
8
|
+
* Reads the AUTHENTICATED user from the session — no `:id` param. Backs
|
|
9
|
+
* `GET /users/me`, which the app's auth client calls on init to RESTORE the
|
|
10
|
+
* session after a full page load.
|
|
11
|
+
*
|
|
12
|
+
* [M1-1] The bearer token is held in memory (a localStorage copy is
|
|
13
|
+
* XSS-exfiltratable and is forbidden), so it is lost on reload; the httpOnly
|
|
14
|
+
* session cookie is the persistent credential. The `auth` middleware reads that
|
|
15
|
+
* cookie into `res.locals.session`, and this handler returns the current user
|
|
16
|
+
* so the client can re-establish `authenticated` state WITHOUT the token ever
|
|
17
|
+
* being exposed to JavaScript. Always self, so it mirrors `read`'s self branch
|
|
18
|
+
* (plan + two-factor status).
|
|
19
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
20
|
+
* @param resource.tableName - The database table name for users.
|
|
21
|
+
* @returns A request handler responding `{ statusCode: 200, body: { props, plan?, twoFactorEnabled? } }`, or 401 when unauthenticated.
|
|
22
|
+
*/
|
|
23
|
+
export const readSelf = ({ tableName }) => {
|
|
24
|
+
return async (_req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const userId = res.locals.session?.userId;
|
|
27
|
+
if (!userId) {
|
|
28
|
+
return {
|
|
29
|
+
statusCode: 401,
|
|
30
|
+
body: {
|
|
31
|
+
error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
|
|
32
|
+
errorKey: 'resource.error.unauthorized',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const user = await findById(tableName, userId);
|
|
37
|
+
if (!user) {
|
|
38
|
+
// Valid session whose user row no longer exists (deleted account holding
|
|
39
|
+
// a still-live cookie) — treat as unauthenticated so the client clears
|
|
40
|
+
// its state, not 404.
|
|
41
|
+
return {
|
|
42
|
+
statusCode: 401,
|
|
43
|
+
body: {
|
|
44
|
+
error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
|
|
45
|
+
errorKey: 'resource.error.unauthorized',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const plan = await getPlan(user);
|
|
50
|
+
const body = { props: stripSensitiveUserColumns(user) };
|
|
51
|
+
if (plan) {
|
|
52
|
+
body.plan = plan;
|
|
53
|
+
}
|
|
54
|
+
// Always self — include two-factor setup info like `read`'s self branch.
|
|
55
|
+
try {
|
|
56
|
+
const secrets = await findById(`${tableName}Secrets`, userId);
|
|
57
|
+
if (secrets) {
|
|
58
|
+
body.twoFactorEnabled = user.twoFactorEnabled || false;
|
|
59
|
+
body.hasPendingTwoFactor = Boolean(secrets.pendingTwoFactorSecret);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (_error) {
|
|
63
|
+
// Non-critical: the secrets table may not exist yet (pre-migration) or the
|
|
64
|
+
// row may be absent. Callers treat absence as false.
|
|
65
|
+
}
|
|
66
|
+
return { statusCode: 200, body };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
logger.error(error);
|
|
70
|
+
return {
|
|
71
|
+
statusCode: 500,
|
|
72
|
+
body: { error: t('user.error.failedToReadUser'), errorKey: 'user.error.failedToReadUser' },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=readSelf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readSelf.js","sourceRoot":"","sources":["../../src/handlers/readSelf.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAItC,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACjD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAA;AAErF,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAE1B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EAAE,SAAS,EAAkB,EAAE,EAAE;IACxD,OAAO,KAAK,EAAE,IAAqB,EAAE,GAAqB,EAAE,EAAE;QAC5D,IAAI,CAAC;YACH,MAAM,MAAM,GAAI,GAAG,CAAC,MAAM,CAAC,OAA2C,EAAE,MAAM,CAAA;YAC9E,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,IAAI,EAAE;wBACJ,KAAK,EAAE,CAAC,CAAC,6BAA6B,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;wBACpF,QAAQ,EAAE,6BAA6B;qBACxC;iBACF,CAAA;YACH,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAc,SAAS,EAAE,MAAM,CAAC,CAAA;YAC3D,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,yEAAyE;gBACzE,uEAAuE;gBACvE,sBAAsB;gBACtB,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,IAAI,EAAE;wBACJ,KAAK,EAAE,CAAC,CAAC,6BAA6B,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;wBACpF,QAAQ,EAAE,6BAA6B;qBACxC;iBACF,CAAA;YACH,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;YAChC,MAAM,IAAI,GAA4B,EAAE,KAAK,EAAE,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAA;YAChF,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;YAClB,CAAC;YAED,yEAAyE;YACzE,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAoB,GAAG,SAAS,SAAS,EAAE,MAAM,CAAC,CAAA;gBAChF,IAAI,OAAO,EAAE,CAAC;oBACZ,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,KAAK,CAAA;oBACtD,IAAI,CAAC,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAA;gBACpE,CAAC;YACH,CAAC;YAAC,OAAO,MAAM,EAAE,CAAC;gBAChB,2EAA2E;gBAC3E,qDAAqD;YACvD,CAAC;YAED,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,6BAA6B,CAAC,EAAE,QAAQ,EAAE,6BAA6B,EAAE;aAC3F,CAAA;QACH,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MoleculeRequest } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/** Request body for confirming a password reset using a token. */
|
|
4
|
+
export interface ResetPasswordRequest extends MoleculeRequest {
|
|
5
|
+
body: {
|
|
6
|
+
token?: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Confirms a password reset by validating the one-time token previously generated by
|
|
12
|
+
* `forgotPassword`, then hashing and storing the new password and clearing the token.
|
|
13
|
+
*
|
|
14
|
+
* Token TTL is one hour. The handler responds with 400 for missing fields, 400 for
|
|
15
|
+
* passwords that fail length validation (matching `updatePassword`), and 401 for
|
|
16
|
+
* tokens that are unknown, expired, or already consumed.
|
|
17
|
+
*
|
|
18
|
+
* On success, all existing device sessions are invalidated to force a fresh login,
|
|
19
|
+
* mirroring the behavior of `updatePassword`.
|
|
20
|
+
*
|
|
21
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
22
|
+
* @param resource.tableName - The database table name for users.
|
|
23
|
+
* @returns A request handler that responds with `{ statusCode: 200, body: { success: true } }` on success.
|
|
24
|
+
*/
|
|
25
|
+
export declare const resetPassword: ({ name: _name, tableName, schema: _schema }: types.Resource) => (req: ResetPasswordRequest) => Promise<{
|
|
26
|
+
statusCode: number;
|
|
27
|
+
body: {
|
|
28
|
+
error: string;
|
|
29
|
+
errorKey: string;
|
|
30
|
+
success?: undefined;
|
|
31
|
+
};
|
|
32
|
+
} | {
|
|
33
|
+
statusCode: number;
|
|
34
|
+
body: {
|
|
35
|
+
success: boolean;
|
|
36
|
+
error?: undefined;
|
|
37
|
+
errorKey?: undefined;
|
|
38
|
+
};
|
|
39
|
+
}>;
|
|
40
|
+
//# sourceMappingURL=resetPassword.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resetPassword.d.ts","sourceRoot":"","sources":["../../src/handlers/resetPassword.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAG7D,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAQzC,kEAAkE;AAClE,MAAM,WAAW,oBAAqB,SAAQ,eAAe;IAC3D,IAAI,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,CAAA;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,GAAI,6CAA6C,KAAK,CAAC,QAAQ,MACzE,KAAK,oBAAoB;;;;;;;;;;;;;;EAoIxC,CAAA"}
|