@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,208 @@
|
|
|
1
|
+
import { v4 as uuid } from 'uuid';
|
|
2
|
+
import { get, getAnalytics, getLogger } from '@molecule/api-bond';
|
|
3
|
+
import { create as storeCreate, findOne } from '@molecule/api-database';
|
|
4
|
+
import { t } from '@molecule/api-i18n';
|
|
5
|
+
import { hash } from '@molecule/api-password';
|
|
6
|
+
import { create as resourceCreate } from '@molecule/api-resource';
|
|
7
|
+
import * as authorization from '../authorization.js';
|
|
8
|
+
import { normalizeEmail } from '../utilities/normalizeEmail.js';
|
|
9
|
+
const analytics = getAnalytics();
|
|
10
|
+
const logger = getLogger();
|
|
11
|
+
/**
|
|
12
|
+
* Creates a user with username and password. Validates username uniqueness and email format,
|
|
13
|
+
* hashes the password into a separate secrets table, creates a session device via the bonded
|
|
14
|
+
* DeviceService, and sets JWT authorization on the response.
|
|
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 that responds with `{ statusCode: 201, body: { props } }` on success.
|
|
20
|
+
*/
|
|
21
|
+
export const create = ({ name, tableName, schema }) => {
|
|
22
|
+
const createResource = resourceCreate({
|
|
23
|
+
name,
|
|
24
|
+
tableName,
|
|
25
|
+
schema,
|
|
26
|
+
});
|
|
27
|
+
return async (req, res) => {
|
|
28
|
+
let body;
|
|
29
|
+
let props;
|
|
30
|
+
try {
|
|
31
|
+
const { password, deviceName } = req.body;
|
|
32
|
+
body = { password, deviceName };
|
|
33
|
+
// SECURITY (P2E-1): build an explicit allow-list of client-writable signup
|
|
34
|
+
// fields. NEVER spread `req.body` into props — that mass-assigns privileged
|
|
35
|
+
// columns. planKey/planExpiresAt/planAutoRenews/emailVerified/oauthServer/
|
|
36
|
+
// oauthId/oauthData/twoFactorEnabled MUST NOT be settable from a signup
|
|
37
|
+
// body (free paid-tier access + email-confirmation bypass + OAuth
|
|
38
|
+
// account-takeover). Those columns are writable only through their
|
|
39
|
+
// dedicated verified flows: updatePlan -> provider confirmation,
|
|
40
|
+
// verifyPayment, the email-confirmation flow, and logInOAuth (which
|
|
41
|
+
// hand-builds its props from the verified provider, not from req.body).
|
|
42
|
+
// Mirrors update.ts, which likewise constructs props field-by-field.
|
|
43
|
+
props = {};
|
|
44
|
+
if (req.body.username !== undefined) {
|
|
45
|
+
// Limit username to 255 alphanumeric characters.
|
|
46
|
+
props.username = String(req.body.username)
|
|
47
|
+
.replace(/[^a-zA-Z0-9]/g, '')
|
|
48
|
+
.substring(0, 255);
|
|
49
|
+
}
|
|
50
|
+
if (req.body.name !== undefined) {
|
|
51
|
+
props.name = String(req.body.name).substring(0, 255);
|
|
52
|
+
}
|
|
53
|
+
if (!body.password) {
|
|
54
|
+
return {
|
|
55
|
+
statusCode: 400,
|
|
56
|
+
body: {
|
|
57
|
+
error: t('user.error.passwordRequired'),
|
|
58
|
+
errorKey: 'user.error.passwordRequired',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (body.password.length > 1024) {
|
|
63
|
+
return {
|
|
64
|
+
statusCode: 400,
|
|
65
|
+
body: {
|
|
66
|
+
error: t('user.error.passwordTooLong', undefined, {
|
|
67
|
+
defaultValue: 'Password must be at most 1024 characters',
|
|
68
|
+
}),
|
|
69
|
+
errorKey: 'user.error.passwordTooLong',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (body.password.length < 8) {
|
|
74
|
+
return {
|
|
75
|
+
statusCode: 400,
|
|
76
|
+
body: {
|
|
77
|
+
error: t('user.error.passwordTooShort', undefined, {
|
|
78
|
+
defaultValue: 'Password must be at least 8 characters',
|
|
79
|
+
}),
|
|
80
|
+
errorKey: 'user.error.passwordTooShort',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (req.body.email !== undefined) {
|
|
85
|
+
if (req.body.email === '' || req.body.email === null) {
|
|
86
|
+
props.email = null;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Normalize (trim + lowercase) BEFORE validation/storage so case and
|
|
90
|
+
// whitespace variants of the same mailbox can't bypass the `email`
|
|
91
|
+
// UNIQUE constraint or the OAuth collision lookup (which also
|
|
92
|
+
// normalizes). Keep the existing length cap.
|
|
93
|
+
const normalizedEmail = normalizeEmail(String(req.body.email))?.substring(0, 1023);
|
|
94
|
+
props.email = normalizedEmail ?? null;
|
|
95
|
+
// Basic email validation
|
|
96
|
+
if (props.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(props.email)) {
|
|
97
|
+
return {
|
|
98
|
+
statusCode: 400,
|
|
99
|
+
body: { error: t('user.error.emailInvalid'), errorKey: 'user.error.emailInvalid' },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
logger.warn('Failed to parse user create request body', { error: err });
|
|
107
|
+
return {
|
|
108
|
+
statusCode: 400,
|
|
109
|
+
body: { error: t('user.error.badRequest'), errorKey: 'user.error.badRequest' },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
// Check if username is taken.
|
|
114
|
+
const usernameTaken = Boolean((await findOne(tableName, [
|
|
115
|
+
{ field: 'username', operator: '=', value: props.username },
|
|
116
|
+
]))?.id);
|
|
117
|
+
if (usernameTaken) {
|
|
118
|
+
return {
|
|
119
|
+
statusCode: 403,
|
|
120
|
+
body: {
|
|
121
|
+
error: t('user.error.usernameUnavailable'),
|
|
122
|
+
errorKey: 'user.error.usernameUnavailable',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (props.email) {
|
|
127
|
+
const emailTaken = Boolean((await findOne(tableName, [
|
|
128
|
+
{ field: 'email', operator: '=', value: props.email },
|
|
129
|
+
]))?.id);
|
|
130
|
+
if (emailTaken) {
|
|
131
|
+
return {
|
|
132
|
+
statusCode: 403,
|
|
133
|
+
body: {
|
|
134
|
+
error: t('user.error.emailAlreadyRegistered'),
|
|
135
|
+
errorKey: 'user.error.emailAlreadyRegistered',
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
logger.error(error);
|
|
143
|
+
}
|
|
144
|
+
// Create the id in advance.
|
|
145
|
+
const id = uuid();
|
|
146
|
+
// Create the user first (usersSecrets has a FK to users.id).
|
|
147
|
+
const createdResponse = await createResource({ props, id: id });
|
|
148
|
+
if (!createdResponse || createdResponse.statusCode !== 201) {
|
|
149
|
+
return createdResponse;
|
|
150
|
+
}
|
|
151
|
+
// Hash and store the password.
|
|
152
|
+
try {
|
|
153
|
+
const passwordHash = await hash(body.password);
|
|
154
|
+
const result = await storeCreate(`${tableName}Secrets`, { id, passwordHash });
|
|
155
|
+
if (!result?.affected) {
|
|
156
|
+
throw new Error(`Failed to insert passwordHash into ${tableName}Secrets.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
logger.error(error);
|
|
161
|
+
return {
|
|
162
|
+
statusCode: 500,
|
|
163
|
+
body: {
|
|
164
|
+
error: t('user.error.failedToHashPassword'),
|
|
165
|
+
errorKey: 'user.error.failedToHashPassword',
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// User and secrets created — now set up the session.
|
|
170
|
+
try {
|
|
171
|
+
const deviceId = await get('device')?.createOrUpdate(id, body.deviceName || 'Unknown');
|
|
172
|
+
if (deviceId) {
|
|
173
|
+
const accessToken = authorization.set(req, res, { userId: id, deviceId });
|
|
174
|
+
analytics
|
|
175
|
+
.track({ name: 'user.signup', userId: id, properties: { method: 'password' } })
|
|
176
|
+
.catch(() => { });
|
|
177
|
+
analytics
|
|
178
|
+
.identify({
|
|
179
|
+
userId: id,
|
|
180
|
+
email: props.email || undefined,
|
|
181
|
+
name: props.name || undefined,
|
|
182
|
+
})
|
|
183
|
+
.catch(() => { });
|
|
184
|
+
// Include accessToken and user in the response body so the frontend
|
|
185
|
+
// auth client can store them (it expects { accessToken, user }).
|
|
186
|
+
const user = createdResponse.body?.props ?? { id, ...props };
|
|
187
|
+
return {
|
|
188
|
+
statusCode: 201,
|
|
189
|
+
body: { props: user, accessToken, user },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
throw new Error('Failed to create session device.');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
logger.error(error);
|
|
198
|
+
return {
|
|
199
|
+
statusCode: 500,
|
|
200
|
+
body: {
|
|
201
|
+
error: t('user.error.failedToCreateSession'),
|
|
202
|
+
errorKey: 'user.error.failedToCreateSession',
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
//# sourceMappingURL=create.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.js","sourceRoot":"","sources":["../../src/handlers/create.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MoleculeRequest } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Deletes a user and their associated data. Removes secrets from the secrets table,
|
|
5
|
+
* devices via the bonded DeviceService, and payment records via the bonded PaymentRecordService
|
|
6
|
+
* before deleting the user row itself.
|
|
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: { id } } }` on success.
|
|
12
|
+
*/
|
|
13
|
+
export declare const del: ({ name: _name, tableName, schema: _schema }: types.Resource) => (req: MoleculeRequest) => Promise<{
|
|
14
|
+
statusCode: number;
|
|
15
|
+
body: {
|
|
16
|
+
error: string;
|
|
17
|
+
errorKey: string;
|
|
18
|
+
props?: undefined;
|
|
19
|
+
};
|
|
20
|
+
} | {
|
|
21
|
+
statusCode: number;
|
|
22
|
+
body: {
|
|
23
|
+
props: {
|
|
24
|
+
id: string;
|
|
25
|
+
};
|
|
26
|
+
error?: undefined;
|
|
27
|
+
errorKey?: undefined;
|
|
28
|
+
};
|
|
29
|
+
}>;
|
|
30
|
+
//# sourceMappingURL=del.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"del.d.ts","sourceRoot":"","sources":["../../src/handlers/del.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAG7D,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAKzC;;;;;;;;;GASG;AACH,eAAO,MAAM,GAAG,GAAI,6CAA6C,KAAK,CAAC,QAAQ,MAC/D,KAAK,eAAe;;;;;;;;;;;;;;;;EA0CnC,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { get, getAnalytics, getLogger } from '@molecule/api-bond';
|
|
2
|
+
import { deleteById } from '@molecule/api-database';
|
|
3
|
+
import { t } from '@molecule/api-i18n';
|
|
4
|
+
import { invalidateAllDeviceExistsCache } from '../authorization.js';
|
|
5
|
+
const analytics = getAnalytics();
|
|
6
|
+
const logger = getLogger();
|
|
7
|
+
/**
|
|
8
|
+
* Deletes a user and their associated data. Removes secrets from the secrets table,
|
|
9
|
+
* devices via the bonded DeviceService, and payment records via the bonded PaymentRecordService
|
|
10
|
+
* before deleting the user row itself.
|
|
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: { id } } }` on success.
|
|
16
|
+
*/
|
|
17
|
+
export const del = ({ name: _name, tableName, schema: _schema }) => {
|
|
18
|
+
return async (req) => {
|
|
19
|
+
try {
|
|
20
|
+
const id = req.params.id;
|
|
21
|
+
// Delete secrets.
|
|
22
|
+
await deleteById(`${tableName}Secrets`, id).catch((err) => {
|
|
23
|
+
logger.warn('Failed to delete user secrets', { userId: id, error: err });
|
|
24
|
+
});
|
|
25
|
+
// Delete devices via bond, then evict this process's positive
|
|
26
|
+
// device-exists cache so any in-flight token for a just-deleted device is
|
|
27
|
+
// rejected on its very next request (immediate in-process revocation).
|
|
28
|
+
await get('device')?.deleteByUserId(id);
|
|
29
|
+
invalidateAllDeviceExistsCache();
|
|
30
|
+
// Delete payments via bond.
|
|
31
|
+
await get('paymentRecords')?.deleteByUserId(id);
|
|
32
|
+
// Delete user.
|
|
33
|
+
const result = await deleteById(tableName, id);
|
|
34
|
+
if (!result?.affected) {
|
|
35
|
+
return {
|
|
36
|
+
statusCode: 404,
|
|
37
|
+
body: { error: t('user.error.notFound'), errorKey: 'user.error.notFound' },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
analytics.track({ name: 'user.deleted', userId: id }).catch(() => { });
|
|
41
|
+
return { statusCode: 200, body: { props: { id } } };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.error(error);
|
|
45
|
+
return {
|
|
46
|
+
statusCode: 500,
|
|
47
|
+
body: {
|
|
48
|
+
error: t('user.error.failedToDeleteUser'),
|
|
49
|
+
errorKey: 'user.error.failedToDeleteUser',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=del.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"del.js","sourceRoot":"","sources":["../../src/handlers/del.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAItC,OAAO,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAA;AAGpE,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;AAChC,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAE1B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAkB,EAAE,EAAE;IACjF,OAAO,KAAK,EAAE,GAAoB,EAAE,EAAE;QACpC,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,EAAY,CAAA;YAElC,kBAAkB;YAClB,MAAM,UAAU,CAAC,GAAG,SAAS,SAAS,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACxD,MAAM,CAAC,IAAI,CAAC,+BAA+B,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;YAC1E,CAAC,CAAC,CAAA;YAEF,8DAA8D;YAC9D,0EAA0E;YAC1E,uEAAuE;YACvE,MAAM,GAAG,CAAoD,QAAQ,CAAC,EAAE,cAAc,CAAC,EAAE,CAAC,CAAA;YAC1F,8BAA8B,EAAE,CAAA;YAEhC,4BAA4B;YAC5B,MAAM,GAAG,CAAuB,gBAAgB,CAAC,EAAE,cAAc,CAAC,EAAE,CAAC,CAAA;YAErE,eAAe;YACf,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;YAE9C,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;gBACtB,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,SAAS,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;YAErE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAA;QACrD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE;oBACJ,KAAK,EAAE,CAAC,CAAC,+BAA+B,CAAC;oBACzC,QAAQ,EAAE,+BAA+B;iBAC1C;aACF,CAAA;QACH,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MoleculeRequest } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/** Request body for password reset initiation. */
|
|
4
|
+
export interface ForgotPasswordRequest extends MoleculeRequest {
|
|
5
|
+
body: {
|
|
6
|
+
email?: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Generates a UUID password reset token, stores it in the secrets table, and sends a reset
|
|
11
|
+
* email via the bonded email provider (if available). Always returns 200 regardless of whether
|
|
12
|
+
* the email exists, to prevent user enumeration.
|
|
13
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
14
|
+
* @param resource.name - The resource name.
|
|
15
|
+
* @param resource.tableName - The database table name for users.
|
|
16
|
+
* @param resource.schema - The validation schema for user properties.
|
|
17
|
+
* @returns A request handler that responds with `{ statusCode: 200, body: { success: true } }`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const forgotPassword: ({ name: _name, tableName, schema: _schema }: types.Resource) => (req: ForgotPasswordRequest) => Promise<{
|
|
20
|
+
statusCode: number;
|
|
21
|
+
body: {
|
|
22
|
+
error: string;
|
|
23
|
+
errorKey: string;
|
|
24
|
+
success?: undefined;
|
|
25
|
+
};
|
|
26
|
+
} | {
|
|
27
|
+
statusCode: number;
|
|
28
|
+
body: {
|
|
29
|
+
success: boolean;
|
|
30
|
+
error?: undefined;
|
|
31
|
+
errorKey?: undefined;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
//# sourceMappingURL=forgotPassword.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forgotPassword.d.ts","sourceRoot":"","sources":["../../src/handlers/forgotPassword.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAOzC,kDAAkD;AAClD,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;CACF;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,6CAA6C,KAAK,CAAC,QAAQ,MAC1E,KAAK,qBAAqB;;;;;;;;;;;;;;EAmIzC,CAAA"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { get, getAnalytics, getLogger } from '@molecule/api-bond';
|
|
3
|
+
import { get as getConfig } from '@molecule/api-config';
|
|
4
|
+
import { findOne, updateById } from '@molecule/api-database';
|
|
5
|
+
import { t } from '@molecule/api-i18n';
|
|
6
|
+
import { hashResetToken } from '../utilities/hashResetToken.js';
|
|
7
|
+
import { normalizeEmail } from '../utilities/normalizeEmail.js';
|
|
8
|
+
const analytics = getAnalytics();
|
|
9
|
+
const logger = getLogger();
|
|
10
|
+
/**
|
|
11
|
+
* Generates a UUID password reset token, stores it in the secrets table, and sends a reset
|
|
12
|
+
* email via the bonded email provider (if available). Always returns 200 regardless of whether
|
|
13
|
+
* the email exists, to prevent user enumeration.
|
|
14
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
15
|
+
* @param resource.name - The resource name.
|
|
16
|
+
* @param resource.tableName - The database table name for users.
|
|
17
|
+
* @param resource.schema - The validation schema for user properties.
|
|
18
|
+
* @returns A request handler that responds with `{ statusCode: 200, body: { success: true } }`.
|
|
19
|
+
*/
|
|
20
|
+
export const forgotPassword = ({ name: _name, tableName, schema: _schema }) => {
|
|
21
|
+
return async (req) => {
|
|
22
|
+
try {
|
|
23
|
+
const { email } = req.body;
|
|
24
|
+
if (!email) {
|
|
25
|
+
return {
|
|
26
|
+
statusCode: 400,
|
|
27
|
+
body: { error: t('user.error.emailRequired'), errorKey: 'user.error.emailRequired' },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Find user by email — normalized to match how signup stores it. A raw
|
|
31
|
+
// mixed-case lookup silently finds nothing, and the 200-no-reveal response
|
|
32
|
+
// (anti-enumeration) then hides the failure: the user never gets a reset.
|
|
33
|
+
const normalizedEmail = normalizeEmail(email);
|
|
34
|
+
const user = normalizedEmail
|
|
35
|
+
? await findOne(tableName, [
|
|
36
|
+
{ field: 'email', operator: '=', value: normalizedEmail },
|
|
37
|
+
])
|
|
38
|
+
: null;
|
|
39
|
+
if (!user) {
|
|
40
|
+
// Don't reveal whether the email exists.
|
|
41
|
+
return { statusCode: 200, body: { success: true } };
|
|
42
|
+
}
|
|
43
|
+
// Generate a reset token. The RAW token is emailed to the user; only its
|
|
44
|
+
// sha256 hash is persisted, so a DB read never yields a usable token.
|
|
45
|
+
const passwordResetToken = crypto.randomBytes(32).toString('hex');
|
|
46
|
+
const passwordResetTokenAt = new Date().toISOString();
|
|
47
|
+
await updateById(`${tableName}Secrets`, user.id, {
|
|
48
|
+
passwordResetToken: hashResetToken(passwordResetToken),
|
|
49
|
+
passwordResetTokenAt,
|
|
50
|
+
});
|
|
51
|
+
// Try to send email if email service is bonded.
|
|
52
|
+
try {
|
|
53
|
+
const emailProvider = get('email');
|
|
54
|
+
if (!emailProvider) {
|
|
55
|
+
// The token was persisted but no reset email can go out: with no
|
|
56
|
+
// email bond the flow silently degrades to "token in the DB nobody
|
|
57
|
+
// ever receives", and the 200-no-reveal response hides it from the
|
|
58
|
+
// caller too. Surface it in the logs so an operator sees the reset
|
|
59
|
+
// path is dead instead of discovering it from user complaints.
|
|
60
|
+
logger.warn('Password reset token created but NO email provider is bonded — the reset email was not sent. Bond an email transport (e.g. @molecule/api-emails-sendgrid) for the reset flow to deliver.', { userId: user.id });
|
|
61
|
+
}
|
|
62
|
+
if (emailProvider) {
|
|
63
|
+
const appName = getConfig('APP_NAME', 'App') ?? 'App';
|
|
64
|
+
const siteOrigin = getConfig('SITE_ORIGIN', '');
|
|
65
|
+
const resetUrl = siteOrigin
|
|
66
|
+
? `${siteOrigin}/reset-password?token=${passwordResetToken}`
|
|
67
|
+
: '';
|
|
68
|
+
// Default the sender to the app's OWN domain (derived from SITE_ORIGIN),
|
|
69
|
+
// never a placeholder like `noreply@example.com`: a provider (e.g. Mailgun)
|
|
70
|
+
// rejects a `from` whose domain isn't its verified sending domain, so a
|
|
71
|
+
// placeholder silently BOUNCES this reset email — a critical flow. An
|
|
72
|
+
// explicit EMAIL_FROM still wins; the localhost fallback is dev-only (prod
|
|
73
|
+
// sets SITE_ORIGIN).
|
|
74
|
+
let defaultFrom = 'no-reply@localhost';
|
|
75
|
+
try {
|
|
76
|
+
if (siteOrigin)
|
|
77
|
+
defaultFrom = `no-reply@${new URL(siteOrigin).hostname}`;
|
|
78
|
+
}
|
|
79
|
+
catch (_error) {
|
|
80
|
+
// Malformed SITE_ORIGIN — keep the localhost fallback.
|
|
81
|
+
}
|
|
82
|
+
await emailProvider.sendMail({
|
|
83
|
+
from: getConfig('EMAIL_FROM', defaultFrom),
|
|
84
|
+
to: email,
|
|
85
|
+
subject: t('user.email.passwordResetSubject', { appName }, { locale: user?.locale || 'en' }),
|
|
86
|
+
subjectKey: 'user.email.passwordResetSubject',
|
|
87
|
+
text: t('user.email.passwordResetText', { token: passwordResetToken }, { locale: user?.locale || 'en' }) +
|
|
88
|
+
'\n\n' +
|
|
89
|
+
(resetUrl
|
|
90
|
+
? t('user.email.passwordResetLink', { url: resetUrl }, { locale: user?.locale || 'en' })
|
|
91
|
+
: ''),
|
|
92
|
+
textKey: 'user.email.passwordResetText',
|
|
93
|
+
html: t('user.email.passwordResetHtml', { token: passwordResetToken }, { locale: user?.locale || 'en' }) +
|
|
94
|
+
(resetUrl
|
|
95
|
+
? t('user.email.passwordResetHtmlLink', { url: resetUrl }, { locale: user?.locale || 'en' })
|
|
96
|
+
: ''),
|
|
97
|
+
htmlKey: 'user.email.passwordResetHtml',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
logger.error('Failed to send password reset email:', error);
|
|
103
|
+
}
|
|
104
|
+
analytics.track({ name: 'user.password_reset_requested', userId: user.id }).catch(() => { });
|
|
105
|
+
return { statusCode: 200, body: { success: true } };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger.error(error);
|
|
109
|
+
return {
|
|
110
|
+
statusCode: 500,
|
|
111
|
+
body: {
|
|
112
|
+
error: t('user.error.failedToProcessPasswordReset'),
|
|
113
|
+
errorKey: 'user.error.failedToProcessPasswordReset',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=forgotPassword.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forgotPassword.js","sourceRoot":"","sources":["../../src/handlers/forgotPassword.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './create.js';
|
|
2
|
+
export * from './del.js';
|
|
3
|
+
export * from './forgotPassword.js';
|
|
4
|
+
export * from './logIn.js';
|
|
5
|
+
export * from './logInOAuth.js';
|
|
6
|
+
export * from './logout.js';
|
|
7
|
+
export * from './oauthAuthorize.js';
|
|
8
|
+
export { handlePaymentNotification, verifyPayment } from './payments/registry.js';
|
|
9
|
+
export * from './read.js';
|
|
10
|
+
export * from './readSelf.js';
|
|
11
|
+
export * from './resetPassword.js';
|
|
12
|
+
export * from './update.js';
|
|
13
|
+
export * from './updatePassword.js';
|
|
14
|
+
export * from './updatePlan.js';
|
|
15
|
+
export * from './verifyTwoFactor.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,qBAAqB,CAAA;AACnC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,OAAO,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACjF,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './create.js';
|
|
2
|
+
export * from './del.js';
|
|
3
|
+
export * from './forgotPassword.js';
|
|
4
|
+
export * from './logIn.js';
|
|
5
|
+
export * from './logInOAuth.js';
|
|
6
|
+
export * from './logout.js';
|
|
7
|
+
export * from './oauthAuthorize.js';
|
|
8
|
+
export { handlePaymentNotification, verifyPayment } from './payments/registry.js';
|
|
9
|
+
export * from './read.js';
|
|
10
|
+
export * from './readSelf.js';
|
|
11
|
+
export * from './resetPassword.js';
|
|
12
|
+
export * from './update.js';
|
|
13
|
+
export * from './updatePassword.js';
|
|
14
|
+
export * from './updatePlan.js';
|
|
15
|
+
export * from './verifyTwoFactor.js';
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,qBAAqB,CAAA;AACnC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,OAAO,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACjF,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
|
|
2
|
+
import type * as types from '../types.js';
|
|
3
|
+
/** Request body for user login, supporting password, reset token, and 2FA flows. */
|
|
4
|
+
export interface LogInRequest extends MoleculeRequest {
|
|
5
|
+
body: {
|
|
6
|
+
username?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
passwordResetToken?: string;
|
|
10
|
+
twoFactorToken?: string;
|
|
11
|
+
deviceName?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Logs in a user by username or email. Supports password authentication, password reset token
|
|
16
|
+
* authentication (1-hour expiry), and optional two-factor verification via `@molecule/api-two-factor`.
|
|
17
|
+
* On success, creates/updates a device via the bonded DeviceService, sets JWT authorization,
|
|
18
|
+
* and notifies other devices about the new login.
|
|
19
|
+
* @param resource - The user resource configuration (name, tableName, schema).
|
|
20
|
+
* @param resource.name - The resource name.
|
|
21
|
+
* @param resource.tableName - The database table name for users.
|
|
22
|
+
* @param resource.schema - The validation schema for user properties.
|
|
23
|
+
* @returns A request handler that responds with `{ statusCode: 200, body: { props } }` on success,
|
|
24
|
+
* or `{ statusCode: 206, body: { twoFactorRequired: true } }` when 2FA is needed.
|
|
25
|
+
*/
|
|
26
|
+
export declare const logIn: ({ name: _name, tableName, schema: _schema }: types.Resource) => (req: LogInRequest, res: MoleculeResponse) => Promise<{
|
|
27
|
+
statusCode: number;
|
|
28
|
+
body: {
|
|
29
|
+
error: string;
|
|
30
|
+
errorKey: string;
|
|
31
|
+
twoFactorRequired?: undefined;
|
|
32
|
+
props?: undefined;
|
|
33
|
+
accessToken?: undefined;
|
|
34
|
+
user?: undefined;
|
|
35
|
+
};
|
|
36
|
+
} | {
|
|
37
|
+
statusCode: number;
|
|
38
|
+
body: {
|
|
39
|
+
twoFactorRequired: boolean;
|
|
40
|
+
error?: undefined;
|
|
41
|
+
errorKey?: undefined;
|
|
42
|
+
props?: undefined;
|
|
43
|
+
accessToken?: undefined;
|
|
44
|
+
user?: undefined;
|
|
45
|
+
};
|
|
46
|
+
} | {
|
|
47
|
+
statusCode: number;
|
|
48
|
+
body: {
|
|
49
|
+
props: {
|
|
50
|
+
id: string;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
username?: string | undefined;
|
|
54
|
+
name?: string | undefined;
|
|
55
|
+
email?: string | null | undefined;
|
|
56
|
+
emailVerified?: boolean | undefined;
|
|
57
|
+
avatar?: string | null | undefined;
|
|
58
|
+
bio?: string | null | undefined;
|
|
59
|
+
twoFactorEnabled?: boolean | undefined;
|
|
60
|
+
oauthServer?: string | undefined;
|
|
61
|
+
oauthId?: string | undefined;
|
|
62
|
+
oauthData?: Record<string, unknown> | undefined;
|
|
63
|
+
planKey?: string | undefined;
|
|
64
|
+
planExpiresAt?: string | undefined;
|
|
65
|
+
planAutoRenews?: boolean | undefined;
|
|
66
|
+
};
|
|
67
|
+
accessToken: string | null;
|
|
68
|
+
user: {
|
|
69
|
+
id: string;
|
|
70
|
+
createdAt: string;
|
|
71
|
+
updatedAt: string;
|
|
72
|
+
username?: string | undefined;
|
|
73
|
+
name?: string | undefined;
|
|
74
|
+
email?: string | null | undefined;
|
|
75
|
+
emailVerified?: boolean | undefined;
|
|
76
|
+
avatar?: string | null | undefined;
|
|
77
|
+
bio?: string | null | undefined;
|
|
78
|
+
twoFactorEnabled?: boolean | undefined;
|
|
79
|
+
oauthServer?: string | undefined;
|
|
80
|
+
oauthId?: string | undefined;
|
|
81
|
+
oauthData?: Record<string, unknown> | undefined;
|
|
82
|
+
planKey?: string | undefined;
|
|
83
|
+
planExpiresAt?: string | undefined;
|
|
84
|
+
planAutoRenews?: boolean | undefined;
|
|
85
|
+
};
|
|
86
|
+
error?: undefined;
|
|
87
|
+
errorKey?: undefined;
|
|
88
|
+
twoFactorRequired?: undefined;
|
|
89
|
+
};
|
|
90
|
+
}>;
|
|
91
|
+
//# sourceMappingURL=logIn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logIn.d.ts","sourceRoot":"","sources":["../../src/handlers/logIn.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAG/E,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAsBzC,oFAAoF;AACpF,MAAM,WAAW,YAAa,SAAQ,eAAe;IACnD,IAAI,EAAE;QACJ,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,KAAK,GAAI,6CAA6C,KAAK,CAAC,QAAQ,MACjE,KAAK,YAAY,EAAE,KAAK,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6MvD,CAAA"}
|