@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.
Files changed (164) hide show
  1. package/LICENSE +115 -0
  2. package/dist/authorization.d.ts +107 -0
  3. package/dist/authorization.d.ts.map +1 -0
  4. package/dist/authorization.js +375 -0
  5. package/dist/authorization.js.map +1 -0
  6. package/dist/authorizers/auth.d.ts +8 -0
  7. package/dist/authorizers/auth.d.ts.map +1 -0
  8. package/dist/authorizers/auth.js +19 -0
  9. package/dist/authorizers/auth.js.map +1 -0
  10. package/dist/authorizers/authSelf.d.ts +14 -0
  11. package/dist/authorizers/authSelf.d.ts.map +1 -0
  12. package/dist/authorizers/authSelf.js +31 -0
  13. package/dist/authorizers/authSelf.js.map +1 -0
  14. package/dist/authorizers/index.d.ts +5 -0
  15. package/dist/authorizers/index.d.ts.map +1 -0
  16. package/dist/authorizers/index.js +5 -0
  17. package/dist/authorizers/index.js.map +1 -0
  18. package/dist/authorizers/rateLimit.d.ts +84 -0
  19. package/dist/authorizers/rateLimit.d.ts.map +1 -0
  20. package/dist/authorizers/rateLimit.js +144 -0
  21. package/dist/authorizers/rateLimit.js.map +1 -0
  22. package/dist/authorizers/requireWebhookAuthenticity.d.ts +23 -0
  23. package/dist/authorizers/requireWebhookAuthenticity.d.ts.map +1 -0
  24. package/dist/authorizers/requireWebhookAuthenticity.js +76 -0
  25. package/dist/authorizers/requireWebhookAuthenticity.js.map +1 -0
  26. package/dist/browser-guard.d.ts +2 -0
  27. package/dist/browser-guard.d.ts.map +1 -0
  28. package/dist/browser-guard.js +19 -0
  29. package/dist/browser-guard.js.map +1 -0
  30. package/dist/handlers/create.d.ts +52 -0
  31. package/dist/handlers/create.d.ts.map +1 -0
  32. package/dist/handlers/create.js +208 -0
  33. package/dist/handlers/create.js.map +1 -0
  34. package/dist/handlers/del.d.ts +30 -0
  35. package/dist/handlers/del.d.ts.map +1 -0
  36. package/dist/handlers/del.js +55 -0
  37. package/dist/handlers/del.js.map +1 -0
  38. package/dist/handlers/forgotPassword.d.ts +34 -0
  39. package/dist/handlers/forgotPassword.d.ts.map +1 -0
  40. package/dist/handlers/forgotPassword.js +119 -0
  41. package/dist/handlers/forgotPassword.js.map +1 -0
  42. package/dist/handlers/index.d.ts +16 -0
  43. package/dist/handlers/index.d.ts.map +1 -0
  44. package/dist/handlers/index.js +16 -0
  45. package/dist/handlers/index.js.map +1 -0
  46. package/dist/handlers/logIn.d.ts +91 -0
  47. package/dist/handlers/logIn.d.ts.map +1 -0
  48. package/dist/handlers/logIn.js +223 -0
  49. package/dist/handlers/logIn.js.map +1 -0
  50. package/dist/handlers/logInOAuth.d.ts +66 -0
  51. package/dist/handlers/logInOAuth.d.ts.map +1 -0
  52. package/dist/handlers/logInOAuth.js +462 -0
  53. package/dist/handlers/logInOAuth.js.map +1 -0
  54. package/dist/handlers/logout.d.ts +22 -0
  55. package/dist/handlers/logout.d.ts.map +1 -0
  56. package/dist/handlers/logout.js +57 -0
  57. package/dist/handlers/logout.js.map +1 -0
  58. package/dist/handlers/oauthAuthorize.d.ts +44 -0
  59. package/dist/handlers/oauthAuthorize.d.ts.map +1 -0
  60. package/dist/handlers/oauthAuthorize.js +141 -0
  61. package/dist/handlers/oauthAuthorize.js.map +1 -0
  62. package/dist/handlers/payments/handlePaymentNotification.d.ts +28 -0
  63. package/dist/handlers/payments/handlePaymentNotification.d.ts.map +1 -0
  64. package/dist/handlers/payments/handlePaymentNotification.js +217 -0
  65. package/dist/handlers/payments/handlePaymentNotification.js.map +1 -0
  66. package/dist/handlers/payments/registry.d.ts +13 -0
  67. package/dist/handlers/payments/registry.d.ts.map +1 -0
  68. package/dist/handlers/payments/registry.js +13 -0
  69. package/dist/handlers/payments/registry.js.map +1 -0
  70. package/dist/handlers/payments/verifyPayment.d.ts +37 -0
  71. package/dist/handlers/payments/verifyPayment.d.ts.map +1 -0
  72. package/dist/handlers/payments/verifyPayment.js +383 -0
  73. package/dist/handlers/payments/verifyPayment.js.map +1 -0
  74. package/dist/handlers/read.d.ts +17 -0
  75. package/dist/handlers/read.d.ts.map +1 -0
  76. package/dist/handlers/read.js +60 -0
  77. package/dist/handlers/read.js.map +1 -0
  78. package/dist/handlers/readSelf.d.ts +23 -0
  79. package/dist/handlers/readSelf.d.ts.map +1 -0
  80. package/dist/handlers/readSelf.js +77 -0
  81. package/dist/handlers/readSelf.js.map +1 -0
  82. package/dist/handlers/resetPassword.d.ts +40 -0
  83. package/dist/handlers/resetPassword.d.ts.map +1 -0
  84. package/dist/handlers/resetPassword.js +147 -0
  85. package/dist/handlers/resetPassword.js.map +1 -0
  86. package/dist/handlers/update.d.ts +28 -0
  87. package/dist/handlers/update.d.ts.map +1 -0
  88. package/dist/handlers/update.js +250 -0
  89. package/dist/handlers/update.js.map +1 -0
  90. package/dist/handlers/updatePassword.d.ts +35 -0
  91. package/dist/handlers/updatePassword.d.ts.map +1 -0
  92. package/dist/handlers/updatePassword.js +113 -0
  93. package/dist/handlers/updatePassword.js.map +1 -0
  94. package/dist/handlers/updatePlan.d.ts +41 -0
  95. package/dist/handlers/updatePlan.d.ts.map +1 -0
  96. package/dist/handlers/updatePlan.js +242 -0
  97. package/dist/handlers/updatePlan.js.map +1 -0
  98. package/dist/handlers/verifyTwoFactor.d.ts +46 -0
  99. package/dist/handlers/verifyTwoFactor.d.ts.map +1 -0
  100. package/dist/handlers/verifyTwoFactor.js +162 -0
  101. package/dist/handlers/verifyTwoFactor.js.map +1 -0
  102. package/dist/i18n.d.ts +10 -0
  103. package/dist/i18n.d.ts.map +1 -0
  104. package/dist/i18n.js +25 -0
  105. package/dist/i18n.js.map +1 -0
  106. package/dist/index.d.ts +88 -0
  107. package/dist/index.d.ts.map +1 -0
  108. package/dist/index.js +88 -0
  109. package/dist/index.js.map +1 -0
  110. package/dist/requestHandlerMap.d.ts +44 -0
  111. package/dist/requestHandlerMap.d.ts.map +1 -0
  112. package/dist/requestHandlerMap.js +68 -0
  113. package/dist/requestHandlerMap.js.map +1 -0
  114. package/dist/resource.d.ts +22 -0
  115. package/dist/resource.d.ts.map +1 -0
  116. package/dist/resource.js +23 -0
  117. package/dist/resource.js.map +1 -0
  118. package/dist/routes.d.ts +44 -0
  119. package/dist/routes.d.ts.map +1 -0
  120. package/dist/routes.js +121 -0
  121. package/dist/routes.js.map +1 -0
  122. package/dist/schema.d.ts +193 -0
  123. package/dist/schema.d.ts.map +1 -0
  124. package/dist/schema.js +229 -0
  125. package/dist/schema.js.map +1 -0
  126. package/dist/secrets.d.ts +16 -0
  127. package/dist/secrets.d.ts.map +1 -0
  128. package/dist/secrets.js +28 -0
  129. package/dist/secrets.js.map +1 -0
  130. package/dist/types.d.ts +14 -0
  131. package/dist/types.d.ts.map +1 -0
  132. package/dist/types.js +9 -0
  133. package/dist/types.js.map +1 -0
  134. package/dist/utilities/getPlan.d.ts +19 -0
  135. package/dist/utilities/getPlan.d.ts.map +1 -0
  136. package/dist/utilities/getPlan.js +26 -0
  137. package/dist/utilities/getPlan.js.map +1 -0
  138. package/dist/utilities/hashResetToken.d.ts +16 -0
  139. package/dist/utilities/hashResetToken.d.ts.map +1 -0
  140. package/dist/utilities/hashResetToken.js +19 -0
  141. package/dist/utilities/hashResetToken.js.map +1 -0
  142. package/dist/utilities/index.d.ts +5 -0
  143. package/dist/utilities/index.d.ts.map +1 -0
  144. package/dist/utilities/index.js +5 -0
  145. package/dist/utilities/index.js.map +1 -0
  146. package/dist/utilities/invalidateEntitlements.d.ts +24 -0
  147. package/dist/utilities/invalidateEntitlements.d.ts.map +1 -0
  148. package/dist/utilities/invalidateEntitlements.js +41 -0
  149. package/dist/utilities/invalidateEntitlements.js.map +1 -0
  150. package/dist/utilities/normalizeEmail.d.ts +22 -0
  151. package/dist/utilities/normalizeEmail.d.ts.map +1 -0
  152. package/dist/utilities/normalizeEmail.js +28 -0
  153. package/dist/utilities/normalizeEmail.js.map +1 -0
  154. package/dist/utilities/notify.d.ts +21 -0
  155. package/dist/utilities/notify.d.ts.map +1 -0
  156. package/dist/utilities/notify.js +39 -0
  157. package/dist/utilities/notify.js.map +1 -0
  158. package/dist/utilities/stripSensitiveUserColumns.d.ts +25 -0
  159. package/dist/utilities/stripSensitiveUserColumns.d.ts.map +1 -0
  160. package/dist/utilities/stripSensitiveUserColumns.js +43 -0
  161. package/dist/utilities/stripSensitiveUserColumns.js.map +1 -0
  162. package/package.json +94 -0
  163. package/setup/users.sql +22 -0
  164. 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,10 @@
1
+ /**
2
+ * Locale registration for the user resource.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * The i18n registered.
8
+ */
9
+ export declare const i18nRegistered = true;
10
+ //# sourceMappingURL=i18n.d.ts.map
@@ -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
@@ -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"}
@@ -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"}