@fonderie/auth 7.23.0 → 7.24.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/brain/outcomes.md +16 -0
- package/brain/signatures.md +4 -6
- package/dist/index.cjs +516 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -4
- package/dist/index.d.ts +21 -4
- package/dist/index.js +510 -55
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +13 -2
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js +13 -2
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/sql/022_account_deletion_requests.sql +17 -0
- package/dist/{session-DZ7QOw1J.d.cts → session-D-KcJlRQ.d.cts} +22 -4
- package/dist/{session-DZ7QOw1J.d.ts → session-D-KcJlRQ.d.ts} +22 -4
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -223,13 +223,16 @@ var schemas_exports = {};
|
|
|
223
223
|
__export(schemas_exports, {
|
|
224
224
|
appleNativeSchema: () => appleNativeSchema,
|
|
225
225
|
changePasswordSchema: () => changePasswordSchema,
|
|
226
|
+
confirmDeletionSchema: () => confirmDeletionSchema,
|
|
226
227
|
forgotPasswordSchema: () => forgotPasswordSchema,
|
|
227
228
|
googleNativeSchema: () => googleNativeSchema,
|
|
228
229
|
loginSchema: () => loginSchema,
|
|
229
230
|
mfaTokenSchema: () => mfaTokenSchema,
|
|
230
231
|
refreshSchema: () => refreshSchema,
|
|
231
232
|
registerSchema: () => registerSchema,
|
|
233
|
+
requestDeletionSchema: () => requestDeletionSchema,
|
|
232
234
|
resetPasswordSchema: () => resetPasswordSchema,
|
|
235
|
+
restoreAccountSchema: () => restoreAccountSchema,
|
|
233
236
|
updateEmailSchema: () => updateEmailSchema,
|
|
234
237
|
updatePhoneSchema: () => updatePhoneSchema,
|
|
235
238
|
updatePreferencesSchema: () => updatePreferencesSchema,
|
|
@@ -309,6 +312,10 @@ var changePasswordSchema = import_zod.z.object({
|
|
|
309
312
|
newPassword: password
|
|
310
313
|
});
|
|
311
314
|
var mfaTokenSchema = import_zod.z.object({ token: import_zod.z.string().trim().min(6).max(64) });
|
|
315
|
+
var secondFactor = import_zod.z.string().trim().min(6).max(16).optional();
|
|
316
|
+
var requestDeletionSchema = import_zod.z.object({ channel: import_zod.z.enum(["email", "sms"]) });
|
|
317
|
+
var confirmDeletionSchema = import_zod.z.object({ code: sixDigitPin, mfaCode: secondFactor });
|
|
318
|
+
var restoreAccountSchema = import_zod.z.object({ restoreToken: import_zod.z.string().min(20).max(4096), mfaCode: secondFactor });
|
|
312
319
|
|
|
313
320
|
// src/services/cookies.ts
|
|
314
321
|
function secureAttr(config) {
|
|
@@ -374,13 +381,23 @@ var MESSAGE_KEYS = {
|
|
|
374
381
|
// arrives already verified by the provider and has nothing to confirm.
|
|
375
382
|
oauthRegistration: "oauth-registration",
|
|
376
383
|
oauthLinked: "oauth-linked",
|
|
377
|
-
oauthUnlinked: "oauth-unlinked"
|
|
384
|
+
oauthUnlinked: "oauth-unlinked",
|
|
385
|
+
// Account deletion (docs/ACCOUNT-DELETION-DESIGN.md). Each goes to the ONE
|
|
386
|
+
// verified channel the person chose — route these types to both 'email'
|
|
387
|
+
// and 'sms' in courier: auth fills in only the chosen address, and the
|
|
388
|
+
// other channel skips. SMS sends each template's text.
|
|
389
|
+
accountDeletionCode: "account-deletion-code",
|
|
390
|
+
accountDeletionScheduled: "account-deletion-scheduled",
|
|
391
|
+
accountRestored: "account-restored"
|
|
378
392
|
};
|
|
379
393
|
var EVENT_KEYS = {
|
|
380
394
|
userRegistered: "fonderie.user.registered",
|
|
381
395
|
userDeleted: "fonderie.user.deleted",
|
|
382
396
|
// Emitted by purgeSoftDeletedUsers (given a bus) once the row is hard-deleted.
|
|
383
397
|
userPurged: "fonderie.user.purged",
|
|
398
|
+
// An archived account was kept: { userId }. Undo what deletion paused
|
|
399
|
+
// (billing resumes a subscription set to end at the period's end).
|
|
400
|
+
userRestored: "fonderie.user.restored",
|
|
384
401
|
emailVerified: "fonderie.user.email_verified",
|
|
385
402
|
passwordChanged: "fonderie.user.password_changed",
|
|
386
403
|
// Sessions the server revoked: { userId, sids (null = all), reason }. Reaches
|
|
@@ -822,6 +839,14 @@ var UserModel = class {
|
|
|
822
839
|
);
|
|
823
840
|
return row ?? null;
|
|
824
841
|
}
|
|
842
|
+
/** An archived account by id (restore). */
|
|
843
|
+
async findArchivedById(id) {
|
|
844
|
+
const [row] = await this.store.query(
|
|
845
|
+
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NOT NULL`,
|
|
846
|
+
[id]
|
|
847
|
+
);
|
|
848
|
+
return row ?? null;
|
|
849
|
+
}
|
|
825
850
|
/** As findArchivedByEmail, for a phone number. */
|
|
826
851
|
async findArchivedByPhone(phone2) {
|
|
827
852
|
const [row] = await this.store.query(
|
|
@@ -1373,10 +1398,17 @@ var BackupCodeModel = class {
|
|
|
1373
1398
|
);
|
|
1374
1399
|
return rows.map((r) => ({ id: r.id, codeHash: r.code_hash }));
|
|
1375
1400
|
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Spend a backup code — once. Conditional on still being unused, so two
|
|
1403
|
+
* requests racing with the same code cannot both sign in: the second finds
|
|
1404
|
+
* it spent (false).
|
|
1405
|
+
*/
|
|
1376
1406
|
async consume(id) {
|
|
1377
|
-
await this.store.query(
|
|
1378
|
-
id
|
|
1379
|
-
|
|
1407
|
+
const rows = await this.store.query(
|
|
1408
|
+
`UPDATE fonderie_mfa_backup_codes SET used_at = now() WHERE id = $1 AND used_at IS NULL RETURNING id`,
|
|
1409
|
+
[id]
|
|
1410
|
+
);
|
|
1411
|
+
return rows.length > 0;
|
|
1380
1412
|
}
|
|
1381
1413
|
async deleteByUser(userId) {
|
|
1382
1414
|
await this.store.query(`DELETE FROM fonderie_mfa_backup_codes WHERE user_id = $1`, [userId]);
|
|
@@ -1516,6 +1548,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1516
1548
|
},
|
|
1517
1549
|
// ── 2. Verify (setup confirm · TOTP login · backup code login) ──
|
|
1518
1550
|
verify: async (ctx) => {
|
|
1551
|
+
if (ctx.user.deletedAt) return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1519
1552
|
const body = ctx.meta["body"];
|
|
1520
1553
|
const token = body?.["token"];
|
|
1521
1554
|
const isLoginCompletion = ctx.user.mfaPending === true;
|
|
@@ -1570,7 +1603,10 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1570
1603
|
if (isLoginCompletion) await failedMfa();
|
|
1571
1604
|
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid backup code");
|
|
1572
1605
|
}
|
|
1573
|
-
await backupCodes.consume(matched.id)
|
|
1606
|
+
if (!await backupCodes.consume(matched.id)) {
|
|
1607
|
+
if (isLoginCompletion) await failedMfa();
|
|
1608
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid backup code");
|
|
1609
|
+
}
|
|
1574
1610
|
} else {
|
|
1575
1611
|
if (!ctx.user.mfaPending) {
|
|
1576
1612
|
return (0, import_core5.setApiResponse)(
|
|
@@ -1697,16 +1733,39 @@ init_password();
|
|
|
1697
1733
|
|
|
1698
1734
|
// src/services/pending-deletion.ts
|
|
1699
1735
|
var import_core6 = require("@fonderie/core");
|
|
1736
|
+
|
|
1737
|
+
// src/services/restore-token.ts
|
|
1738
|
+
var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
|
|
1739
|
+
var RESTORE_TTL = "10m";
|
|
1740
|
+
function issueRestoreToken(userId, deletedAt, loginMethod, config) {
|
|
1741
|
+
return import_jsonwebtoken2.default.sign(
|
|
1742
|
+
{ sub: userId, type: "restore", loginMethod, archivedAt: deletedAt.toISOString() },
|
|
1743
|
+
config.jwtSecret,
|
|
1744
|
+
{ expiresIn: RESTORE_TTL, keyid: keyIdOf(config.jwtSecret) }
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
function verifyRestoreToken(token, config) {
|
|
1748
|
+
const payload = verifyToken(token, config);
|
|
1749
|
+
if (!payload || payload.type !== "restore" || typeof payload.sub !== "string" || typeof payload.archivedAt !== "string") return null;
|
|
1750
|
+
return payload;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/services/pending-deletion.ts
|
|
1700
1754
|
function deletionDate(deletedAt, config) {
|
|
1701
1755
|
return new Date(deletedAt.getTime() + deletionGracePeriodDays(config) * 24 * 60 * 60 * 1e3);
|
|
1702
1756
|
}
|
|
1703
|
-
function pendingDeletionResponse(
|
|
1704
|
-
const deleteOn = deletionDate(deletedAt, config);
|
|
1757
|
+
function pendingDeletionResponse(user, loginMethod, config) {
|
|
1758
|
+
const deleteOn = deletionDate(user.deletedAt, config);
|
|
1705
1759
|
return (0, import_core6.setApiResponse)(
|
|
1706
1760
|
import_core6.HTTP.FORBIDDEN,
|
|
1707
1761
|
"ACCOUNT_PENDING_DELETION",
|
|
1708
1762
|
`This account is scheduled for deletion on ${deleteOn.toISOString().slice(0, 10)}.`,
|
|
1709
|
-
{
|
|
1763
|
+
{
|
|
1764
|
+
requestedAt: user.deletedAt.toISOString(),
|
|
1765
|
+
deleteOn: deleteOn.toISOString(),
|
|
1766
|
+
restoreToken: issueRestoreToken(user.id, user.deletedAt, loginMethod, config),
|
|
1767
|
+
mfaRequired: user.mfaEnabled === true
|
|
1768
|
+
}
|
|
1710
1769
|
);
|
|
1711
1770
|
}
|
|
1712
1771
|
function archivedAddressResponse() {
|
|
@@ -1717,6 +1776,27 @@ function archivedAddressResponse() {
|
|
|
1717
1776
|
);
|
|
1718
1777
|
}
|
|
1719
1778
|
|
|
1779
|
+
// src/services/second-factor.ts
|
|
1780
|
+
init_password();
|
|
1781
|
+
async function verifySecondFactor(store, config, userId, code) {
|
|
1782
|
+
if (typeof code !== "string") return false;
|
|
1783
|
+
const value = code.trim();
|
|
1784
|
+
if (/^\d{6}$/.test(value)) {
|
|
1785
|
+
const stored = await new UserModel(store).getMfaSecret(userId);
|
|
1786
|
+
if (!stored) return false;
|
|
1787
|
+
return verifyTotpToken(value, makeMfaCipher(config.mfaSecretKey).decrypt(stored));
|
|
1788
|
+
}
|
|
1789
|
+
if (/^[A-Z0-9]{8}$/i.test(value)) {
|
|
1790
|
+
const backupCodes = new BackupCodeModel(store);
|
|
1791
|
+
for (const row of await backupCodes.findUnused(userId)) {
|
|
1792
|
+
if (await verifyPassword(value.toUpperCase(), row.codeHash)) {
|
|
1793
|
+
return backupCodes.consume(row.id);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1720
1800
|
// src/models/password-reset.model.ts
|
|
1721
1801
|
var import_node_crypto5 = require("crypto");
|
|
1722
1802
|
var hashSecret = (value) => (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
|
|
@@ -2057,7 +2137,7 @@ function authController(store, config, bus, locales = (0, import_core7.defineLoc
|
|
|
2057
2137
|
failureReason: valid2 ? "pending_deletion" : "bad_password",
|
|
2058
2138
|
...meta
|
|
2059
2139
|
});
|
|
2060
|
-
if (valid2) return pendingDeletionResponse(archived.deletedAt, config);
|
|
2140
|
+
if (valid2) return pendingDeletionResponse({ ...archived, deletedAt: archived.deletedAt }, "email", config);
|
|
2061
2141
|
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2062
2142
|
}
|
|
2063
2143
|
}
|
|
@@ -2135,7 +2215,7 @@ function authController(store, config, bus, locales = (0, import_core7.defineLoc
|
|
|
2135
2215
|
}
|
|
2136
2216
|
const phone2 = body?.["phone"];
|
|
2137
2217
|
if (isValidPhone(phone2)) {
|
|
2138
|
-
const user = await users.findByPhone(normalizePhone(phone2));
|
|
2218
|
+
const user = await users.findByPhone(normalizePhone(phone2)) ?? await users.findArchivedByPhone(normalizePhone(phone2));
|
|
2139
2219
|
if (!user) {
|
|
2140
2220
|
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2141
2221
|
}
|
|
@@ -2171,6 +2251,65 @@ function authController(store, config, bus, locales = (0, import_core7.defineLoc
|
|
|
2171
2251
|
"Provide email + password or a valid phone number"
|
|
2172
2252
|
);
|
|
2173
2253
|
},
|
|
2254
|
+
/**
|
|
2255
|
+
* "Keep my account": the restoreToken from a sign-in to an archived account
|
|
2256
|
+
* (proof already given: password, Google / Apple, phone code), plus a
|
|
2257
|
+
* second factor when two-factor is on. Un-archives in one statement —
|
|
2258
|
+
* only the SAME archive the proof was for — then signs the person in.
|
|
2259
|
+
*/
|
|
2260
|
+
restoreAccount: async (ctx) => {
|
|
2261
|
+
const body = ctx.meta["body"];
|
|
2262
|
+
const proof = verifyRestoreToken(String(body?.["restoreToken"] ?? ""), config);
|
|
2263
|
+
const invalid = () => (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "RESTORE_TOKEN_INVALID", "Sign in again to keep your account.");
|
|
2264
|
+
if (!proof) return invalid();
|
|
2265
|
+
const archived = await users.findArchivedById(proof.sub);
|
|
2266
|
+
if (!archived?.deletedAt || archived.deletedAt.toISOString() !== proof.archivedAt) return invalid();
|
|
2267
|
+
if (archived.suspended) {
|
|
2268
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.FORBIDDEN, "ACCOUNT_SUSPENDED", "Account suspended. Please contact support.");
|
|
2269
|
+
}
|
|
2270
|
+
if (archived.mfaEnabled && !await verifySecondFactor(store, config, archived.id, body?.["mfaCode"])) {
|
|
2271
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "MFA_REQUIRED", "Enter a code from your authenticator app (or a backup code) to keep your account.");
|
|
2272
|
+
}
|
|
2273
|
+
const [restored] = await store.query(
|
|
2274
|
+
`UPDATE fonderie_users
|
|
2275
|
+
SET deleted_at = NULL, deletion_channel = NULL, deletion_reminded_at = NULL, updated_at = now()
|
|
2276
|
+
-- The SAME archive the proof was for. Postgres keeps microseconds,
|
|
2277
|
+
-- the proof (a JS date) milliseconds: compare at that precision.
|
|
2278
|
+
WHERE id = $1 AND date_trunc('milliseconds', deleted_at) = date_trunc('milliseconds', $2::timestamptz)
|
|
2279
|
+
RETURNING (SELECT deletion_channel FROM fonderie_users WHERE id = $1) AS channel`,
|
|
2280
|
+
[archived.id, proof.archivedAt]
|
|
2281
|
+
);
|
|
2282
|
+
if (!restored) return invalid();
|
|
2283
|
+
const meta = requestMeta(ctx);
|
|
2284
|
+
const method = proof.loginMethod === "google" ? "oauth-google" : proof.loginMethod === "apple" ? "oauth-apple" : proof.loginMethod === "phone" ? "phone" : "password";
|
|
2285
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(archived.id, configForClient(config, config.resolve?.(ctx), clientKindOf(ctx.request.headers)), {
|
|
2286
|
+
loginMethod: proof.loginMethod,
|
|
2287
|
+
phoneVerified: proof.loginMethod === "phone"
|
|
2288
|
+
});
|
|
2289
|
+
await sessions.create(archived.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
2290
|
+
await loginEvents.recordSafe({ userId: archived.id, emailAttempted: archived.email, method, outcome: "success", ...meta });
|
|
2291
|
+
const reqId = ctx.meta["requestId"];
|
|
2292
|
+
await (0, import_core7.background)(bus?.emit(EVENT_KEYS.userRestored, { userId: archived.id }, reqId !== void 0 ? { requestId: reqId } : void 0));
|
|
2293
|
+
const viaSms = restored.channel === "sms" || !archived.email && !!archived.phone;
|
|
2294
|
+
const address = viaSms ? archived.phone : archived.email;
|
|
2295
|
+
if (address) {
|
|
2296
|
+
await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
2297
|
+
type: MESSAGE_KEYS.accountRestored,
|
|
2298
|
+
locale: archived.locale,
|
|
2299
|
+
data: {},
|
|
2300
|
+
recipient: viaSms ? { email: null, phone: address, deviceToken: null } : { email: address, phone: null, deviceToken: null }
|
|
2301
|
+
}));
|
|
2302
|
+
}
|
|
2303
|
+
const user = await users.findById(archived.id);
|
|
2304
|
+
return Response.json(
|
|
2305
|
+
{
|
|
2306
|
+
reason: "ACCOUNT_RESTORED",
|
|
2307
|
+
explanation: "Your account is active again.",
|
|
2308
|
+
result: { tokens: { access: accessToken, refresh: refreshToken }, user: user ? toUserDTO(user) : null }
|
|
2309
|
+
},
|
|
2310
|
+
{ status: 200, headers: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)) }
|
|
2311
|
+
);
|
|
2312
|
+
},
|
|
2174
2313
|
logout: async (ctx) => {
|
|
2175
2314
|
const sid = ctx.user?.sid;
|
|
2176
2315
|
if (sid) {
|
|
@@ -2309,8 +2448,10 @@ function authController(store, config, bus, locales = (0, import_core7.defineLoc
|
|
|
2309
2448
|
);
|
|
2310
2449
|
}
|
|
2311
2450
|
let row;
|
|
2451
|
+
let presented;
|
|
2312
2452
|
if (typeof rawToken === "string" && rawToken.trim().length >= 32) {
|
|
2313
|
-
|
|
2453
|
+
presented = rawToken.trim();
|
|
2454
|
+
row = await passwordReset.findByToken(presented);
|
|
2314
2455
|
} else {
|
|
2315
2456
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
2316
2457
|
return (0, import_core7.setApiResponse)(
|
|
@@ -2319,7 +2460,8 @@ function authController(store, config, bus, locales = (0, import_core7.defineLoc
|
|
|
2319
2460
|
"a 6-digit pin or a reset token is required"
|
|
2320
2461
|
);
|
|
2321
2462
|
}
|
|
2322
|
-
|
|
2463
|
+
presented = raw.trim();
|
|
2464
|
+
row = await passwordReset.findByPin(presented);
|
|
2323
2465
|
}
|
|
2324
2466
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
2325
2467
|
return (0, import_core7.setApiResponse)(
|
|
@@ -2330,17 +2472,22 @@ function authController(store, config, bus, locales = (0, import_core7.defineLoc
|
|
|
2330
2472
|
}
|
|
2331
2473
|
const passwordHash = await hashPassword(password2);
|
|
2332
2474
|
const [changed] = await store.query(
|
|
2333
|
-
`WITH
|
|
2475
|
+
`WITH spent AS (
|
|
2476
|
+
-- The code is spent HERE, and only what this statement spent
|
|
2477
|
+
-- changes a password: two resets racing with one code \u2014 the
|
|
2478
|
+
-- second finds it gone and changes nothing.
|
|
2479
|
+
DELETE FROM fonderie_password_resets
|
|
2480
|
+
WHERE user_id = $2 AND (pin = $3 OR token = $3) AND expires_at > now()
|
|
2481
|
+
RETURNING user_id
|
|
2482
|
+
), changed AS (
|
|
2334
2483
|
UPDATE fonderie_users SET password_hash = $1, updated_at = now()
|
|
2335
|
-
WHERE id
|
|
2484
|
+
WHERE id IN (SELECT user_id FROM spent) AND deleted_at IS NULL
|
|
2336
2485
|
RETURNING id
|
|
2337
|
-
), spent AS (
|
|
2338
|
-
DELETE FROM fonderie_password_resets WHERE user_id = $2
|
|
2339
2486
|
), revoked AS (
|
|
2340
2487
|
DELETE FROM fonderie_sessions WHERE user_id IN (SELECT id FROM changed)
|
|
2341
2488
|
)
|
|
2342
2489
|
SELECT id FROM changed`,
|
|
2343
|
-
[passwordHash, row.userId]
|
|
2490
|
+
[passwordHash, row.userId, hashSecret(presented)]
|
|
2344
2491
|
);
|
|
2345
2492
|
if (!changed) {
|
|
2346
2493
|
return (0, import_core7.setApiResponse)(
|
|
@@ -2402,6 +2549,17 @@ function authController(store, config, bus, locales = (0, import_core7.defineLoc
|
|
|
2402
2549
|
);
|
|
2403
2550
|
}
|
|
2404
2551
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
2552
|
+
if (ctx.user.deletedAt) {
|
|
2553
|
+
await loginEvents.recordSafe({
|
|
2554
|
+
userId: ctx.user.id,
|
|
2555
|
+
emailAttempted: null,
|
|
2556
|
+
method: "phone",
|
|
2557
|
+
outcome: "failed",
|
|
2558
|
+
failureReason: "pending_deletion",
|
|
2559
|
+
...phoneMeta
|
|
2560
|
+
});
|
|
2561
|
+
return pendingDeletionResponse({ ...ctx.user, deletedAt: ctx.user.deletedAt }, "phone", config);
|
|
2562
|
+
}
|
|
2405
2563
|
const { accessToken, refreshToken, sid } = issueTokenPair(ctx.user.id, configForClient(config, config.resolve?.(ctx), clientKindOf(ctx.request.headers)), {
|
|
2406
2564
|
loginMethod: "phone",
|
|
2407
2565
|
phoneVerified: true
|
|
@@ -2546,7 +2704,7 @@ function authController(store, config, bus, locales = (0, import_core7.defineLoc
|
|
|
2546
2704
|
}
|
|
2547
2705
|
|
|
2548
2706
|
// src/controllers/user.controller.ts
|
|
2549
|
-
var
|
|
2707
|
+
var import_node_crypto8 = require("crypto");
|
|
2550
2708
|
var import_core9 = require("@fonderie/core");
|
|
2551
2709
|
var import_events3 = require("@fonderie/events");
|
|
2552
2710
|
|
|
@@ -2585,7 +2743,64 @@ function toSessionDTO(row, currentSid) {
|
|
|
2585
2743
|
};
|
|
2586
2744
|
}
|
|
2587
2745
|
|
|
2746
|
+
// src/models/account-deletion.model.ts
|
|
2747
|
+
var import_node_crypto7 = require("crypto");
|
|
2748
|
+
var hashCode = (code) => (0, import_node_crypto7.createHash)("sha256").update(code).digest("hex");
|
|
2749
|
+
var DELETION_CODE_MAX_ATTEMPTS = 5;
|
|
2750
|
+
var AccountDeletionModel = class {
|
|
2751
|
+
constructor(store) {
|
|
2752
|
+
this.store = store;
|
|
2753
|
+
}
|
|
2754
|
+
store;
|
|
2755
|
+
/** Replace any pending code (a new request supersedes the old one). */
|
|
2756
|
+
async saveCode(userId, code, channel, expiresAt) {
|
|
2757
|
+
await this.store.query(
|
|
2758
|
+
`INSERT INTO fonderie_account_deletion_codes (user_id, code_hash, channel, attempts, expires_at, created_at)
|
|
2759
|
+
VALUES ($1, $2, $3, 0, $4, now())
|
|
2760
|
+
ON CONFLICT (user_id) DO UPDATE
|
|
2761
|
+
SET code_hash = $2, channel = $3, attempts = 0, expires_at = $4, created_at = now()`,
|
|
2762
|
+
[userId, hashCode(code), channel, expiresAt]
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
async lastSentAt(userId) {
|
|
2766
|
+
const [row] = await this.store.query(
|
|
2767
|
+
`SELECT created_at AS "createdAt" FROM fonderie_account_deletion_codes WHERE user_id = $1`,
|
|
2768
|
+
[userId]
|
|
2769
|
+
);
|
|
2770
|
+
return row ? new Date(row.createdAt) : null;
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Check a code in ONE statement: a wrong code spends an attempt, a right
|
|
2774
|
+
* one is consumed (deleted) — so a code is good once, and a guesser gets
|
|
2775
|
+
* five tries in total however many requests they race.
|
|
2776
|
+
*/
|
|
2777
|
+
async checkCode(userId, code) {
|
|
2778
|
+
const [row] = await this.store.query(
|
|
2779
|
+
`WITH current AS (
|
|
2780
|
+
SELECT user_id, code_hash = $2 AS matched, expires_at <= now() AS expired, attempts, channel
|
|
2781
|
+
FROM fonderie_account_deletion_codes WHERE user_id = $1
|
|
2782
|
+
FOR UPDATE
|
|
2783
|
+
), spent AS (
|
|
2784
|
+
DELETE FROM fonderie_account_deletion_codes d USING current c
|
|
2785
|
+
WHERE d.user_id = c.user_id AND c.matched AND NOT c.expired AND c.attempts < $3
|
|
2786
|
+
), missed AS (
|
|
2787
|
+
UPDATE fonderie_account_deletion_codes d SET attempts = d.attempts + 1
|
|
2788
|
+
FROM current c
|
|
2789
|
+
WHERE d.user_id = c.user_id AND NOT (c.matched AND NOT c.expired AND c.attempts < $3)
|
|
2790
|
+
)
|
|
2791
|
+
SELECT matched, expired, attempts, channel FROM current`,
|
|
2792
|
+
[userId, hashCode(code), DELETION_CODE_MAX_ATTEMPTS]
|
|
2793
|
+
);
|
|
2794
|
+
if (!row) return { result: "none" };
|
|
2795
|
+
if (row.attempts >= DELETION_CODE_MAX_ATTEMPTS) return { result: "exhausted" };
|
|
2796
|
+
if (row.expired) return { result: "expired" };
|
|
2797
|
+
return { result: row.matched ? "ok" : "invalid", channel: row.channel };
|
|
2798
|
+
}
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2588
2801
|
// src/controllers/user.controller.ts
|
|
2802
|
+
var DELETION_CODE_TTL_MS = 15 * 60 * 1e3;
|
|
2803
|
+
var DELETION_CODE_COOLDOWN_MS = 60 * 1e3;
|
|
2589
2804
|
function normalizePhone2(phone2) {
|
|
2590
2805
|
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
2591
2806
|
}
|
|
@@ -2598,6 +2813,55 @@ function userController(store, config, bus) {
|
|
|
2598
2813
|
const loginEvents = new LoginEventModel(store, config.location);
|
|
2599
2814
|
const emailVerif = new EmailVerificationModel(store);
|
|
2600
2815
|
const phoneVerif = new PhoneVerificationModel(store);
|
|
2816
|
+
const deletions = new AccountDeletionModel(store);
|
|
2817
|
+
const deletionBlocked = async (userId) => {
|
|
2818
|
+
for (const blocker of config.accountDeletion?.blockers ?? []) {
|
|
2819
|
+
const refusal = await blocker(userId);
|
|
2820
|
+
if (refusal) return (0, import_core9.setApiResponse)(import_core9.HTTP.CONFLICT, refusal.reason, refusal.explanation, refusal.details);
|
|
2821
|
+
}
|
|
2822
|
+
return null;
|
|
2823
|
+
};
|
|
2824
|
+
const archive = async (userId, channel) => {
|
|
2825
|
+
const [row] = await store.query(
|
|
2826
|
+
`WITH archived AS (
|
|
2827
|
+
UPDATE fonderie_users
|
|
2828
|
+
SET deleted_at = now(), updated_at = now(), deletion_channel = $2, deletion_reminded_at = NULL
|
|
2829
|
+
WHERE id = $1 AND deleted_at IS NULL
|
|
2830
|
+
RETURNING id, deleted_at
|
|
2831
|
+
), sessions_ended AS (
|
|
2832
|
+
DELETE FROM fonderie_sessions WHERE user_id = $1
|
|
2833
|
+
), resets AS (
|
|
2834
|
+
DELETE FROM fonderie_password_resets WHERE user_id = $1
|
|
2835
|
+
), emails AS (
|
|
2836
|
+
DELETE FROM fonderie_email_verifications WHERE user_id = $1
|
|
2837
|
+
), phones AS (
|
|
2838
|
+
DELETE FROM fonderie_phone_verifications WHERE user_id = $1
|
|
2839
|
+
), codes AS (
|
|
2840
|
+
DELETE FROM fonderie_account_deletion_codes WHERE user_id = $1
|
|
2841
|
+
)
|
|
2842
|
+
SELECT deleted_at AS "deletedAt" FROM archived`,
|
|
2843
|
+
[userId, channel]
|
|
2844
|
+
);
|
|
2845
|
+
return row ? new Date(row.deletedAt) : /* @__PURE__ */ new Date();
|
|
2846
|
+
};
|
|
2847
|
+
const announceArchived = async (ctx, deletedAt) => {
|
|
2848
|
+
const userId = ctx.user.id;
|
|
2849
|
+
const reqId = ctx.meta["requestId"];
|
|
2850
|
+
await (0, import_core9.background)(bus?.emit(
|
|
2851
|
+
EVENT_KEYS.userDeleted,
|
|
2852
|
+
{ userId, deletedAt: deletedAt.toISOString(), deleteOn: deletionDate(deletedAt, config).toISOString() },
|
|
2853
|
+
reqId !== void 0 ? { requestId: reqId } : void 0
|
|
2854
|
+
));
|
|
2855
|
+
await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId, sids: null, reason: "account-deleted" }));
|
|
2856
|
+
};
|
|
2857
|
+
const archivedResponse = (deletedAt) => Response.json(
|
|
2858
|
+
{
|
|
2859
|
+
reason: "ACCOUNT_DELETED",
|
|
2860
|
+
explanation: "Account closed. It will be permanently deleted on the date shown; sign in before then to keep it.",
|
|
2861
|
+
result: { requestedAt: deletedAt.toISOString(), deleteOn: deletionDate(deletedAt, config).toISOString() }
|
|
2862
|
+
},
|
|
2863
|
+
{ status: 200, headers: cookieHeaders(clearedTokenCookies(config)) }
|
|
2864
|
+
);
|
|
2601
2865
|
return {
|
|
2602
2866
|
me: async (ctx) => {
|
|
2603
2867
|
const user = await users.findById(ctx.user.id);
|
|
@@ -2744,7 +3008,7 @@ function userController(store, config, bus) {
|
|
|
2744
3008
|
if (existing) {
|
|
2745
3009
|
return (0, import_core9.setApiResponse)(import_core9.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
2746
3010
|
}
|
|
2747
|
-
const pin = (0,
|
|
3011
|
+
const pin = (0, import_node_crypto8.randomInt)(1e5, 1e6).toString();
|
|
2748
3012
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2749
3013
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2750
3014
|
await users.updateEmail(ctx.user.id, normalised);
|
|
@@ -2786,7 +3050,7 @@ function userController(store, config, bus) {
|
|
|
2786
3050
|
if (existing) {
|
|
2787
3051
|
return (0, import_core9.setApiResponse)(import_core9.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
2788
3052
|
}
|
|
2789
|
-
const otp = (0,
|
|
3053
|
+
const otp = (0, import_node_crypto8.randomInt)(1e5, 1e6).toString();
|
|
2790
3054
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
2791
3055
|
await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
|
|
2792
3056
|
await users.updatePhone(ctx.user.id, normalised);
|
|
@@ -2880,30 +3144,95 @@ function userController(store, config, bus) {
|
|
|
2880
3144
|
}
|
|
2881
3145
|
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
|
|
2882
3146
|
},
|
|
3147
|
+
// Legacy one-call deletion (no code). Kept so existing apps keep working;
|
|
3148
|
+
// apps move to POST /users/me/deletion + /confirm (proof + notices).
|
|
2883
3149
|
deleteMe: async (ctx) => {
|
|
2884
|
-
const
|
|
2885
|
-
await
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
)
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3150
|
+
const deletedAt = await archive(ctx.user.id, null);
|
|
3151
|
+
await announceArchived(ctx, deletedAt);
|
|
3152
|
+
return archivedResponse(deletedAt);
|
|
3153
|
+
},
|
|
3154
|
+
/**
|
|
3155
|
+
* Step 1 of deleting an account: a 6-digit code to the channel the person
|
|
3156
|
+
* picks ('email' | 'sms' — an address on the account; receiving the code
|
|
3157
|
+
* proves they hold it). Refused while a blocker objects (e.g. they own a
|
|
3158
|
+
* team with other members — transfer it first).
|
|
3159
|
+
*/
|
|
3160
|
+
requestDeletion: async (ctx) => {
|
|
3161
|
+
const user = ctx.user;
|
|
3162
|
+
const body = ctx.meta["body"];
|
|
3163
|
+
const channel = body?.["channel"] === "sms" ? "sms" : body?.["channel"] === "email" ? "email" : null;
|
|
3164
|
+
if (!channel) {
|
|
3165
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "channel must be 'email' or 'sms'");
|
|
3166
|
+
}
|
|
3167
|
+
const address = channel === "email" ? user.email : user.phone;
|
|
3168
|
+
if (!address) {
|
|
3169
|
+
return (0, import_core9.setApiResponse)(
|
|
3170
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
3171
|
+
channel === "email" ? "NO_EMAIL_ON_ACCOUNT" : "NO_PHONE_ON_ACCOUNT",
|
|
3172
|
+
`There is no ${channel === "email" ? "email address" : "phone number"} on this account to send the code to.`
|
|
3173
|
+
);
|
|
3174
|
+
}
|
|
3175
|
+
const refused = await deletionBlocked(user.id);
|
|
3176
|
+
if (refused) return refused;
|
|
3177
|
+
const remainingMs = checkCooldown(await deletions.lastSentAt(user.id), DELETION_CODE_COOLDOWN_MS);
|
|
3178
|
+
if (remainingMs > 0) {
|
|
3179
|
+
const retryAfter = Math.ceil(remainingMs / 1e3);
|
|
3180
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.TOO_MANY_REQUESTS, "VERIFICATION_COOLDOWN", `Wait ${retryAfter}s before requesting a new code.`, { retryAfter });
|
|
3181
|
+
}
|
|
3182
|
+
const code = (0, import_node_crypto8.randomInt)(1e5, 1e6).toString();
|
|
3183
|
+
await deletions.saveCode(user.id, code, channel, new Date(Date.now() + DELETION_CODE_TTL_MS));
|
|
3184
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
3185
|
+
type: MESSAGE_KEYS.accountDeletionCode,
|
|
3186
|
+
locale: user.locale,
|
|
3187
|
+
data: { code },
|
|
3188
|
+
recipient: channel === "email" ? { email: address, phone: null, deviceToken: null } : { email: null, phone: address, deviceToken: null }
|
|
3189
|
+
}));
|
|
3190
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.ACCEPTED, "ACCOUNT_DELETION_CODE_SENT", "A code to confirm the deletion was sent.", {
|
|
3191
|
+
channel,
|
|
3192
|
+
expiresInSeconds: DELETION_CODE_TTL_MS / 1e3,
|
|
3193
|
+
mfaRequired: user.mfaEnabled === true
|
|
3194
|
+
});
|
|
3195
|
+
},
|
|
3196
|
+
/**
|
|
3197
|
+
* Step 2: the code (and, with two-factor on, a second factor) archives the
|
|
3198
|
+
* account — closed at once, restorable at sign-in until the purge date.
|
|
3199
|
+
*/
|
|
3200
|
+
confirmDeletion: async (ctx) => {
|
|
3201
|
+
const user = ctx.user;
|
|
3202
|
+
const body = ctx.meta["body"];
|
|
3203
|
+
const code = typeof body?.["code"] === "string" ? body["code"].trim() : "";
|
|
3204
|
+
if (!/^\d{6}$/.test(code)) {
|
|
3205
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "code must be the 6-digit code that was sent");
|
|
3206
|
+
}
|
|
3207
|
+
const refused = await deletionBlocked(user.id);
|
|
3208
|
+
if (refused) return refused;
|
|
3209
|
+
if (user.mfaEnabled && !await verifySecondFactor(store, config, user.id, body?.["mfaCode"])) {
|
|
3210
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNAUTHORIZED, "MFA_REQUIRED", "Enter a code from your authenticator app (or a backup code) to confirm.");
|
|
3211
|
+
}
|
|
3212
|
+
const checked = await deletions.checkCode(user.id, code);
|
|
3213
|
+
if (checked.result !== "ok") {
|
|
3214
|
+
return (0, import_core9.setApiResponse)(
|
|
3215
|
+
import_core9.HTTP.BAD_REQUEST,
|
|
3216
|
+
"VERIFICATION_FAILED",
|
|
3217
|
+
checked.result === "exhausted" || checked.result === "none" || checked.result === "expired" ? "This code can no longer be used. Request a new one." : "That code is incorrect."
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
const deletedAt = await archive(user.id, checked.channel ?? null);
|
|
3221
|
+
await announceArchived(ctx, deletedAt);
|
|
3222
|
+
const deleteOn = deletionDate(deletedAt, config);
|
|
3223
|
+
const address = checked.channel === "sms" ? user.phone : user.email;
|
|
3224
|
+
if (address) {
|
|
3225
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
3226
|
+
type: MESSAGE_KEYS.accountDeletionScheduled,
|
|
3227
|
+
locale: user.locale,
|
|
3228
|
+
data: {
|
|
3229
|
+
deleteOn: deleteOn.toISOString().slice(0, 10),
|
|
3230
|
+
[import_core9.COURIER_FORMAT_KEY]: { deleteOn: { date: deleteOn.toISOString(), style: "long" } }
|
|
3231
|
+
},
|
|
3232
|
+
recipient: checked.channel === "sms" ? { email: null, phone: address, deviceToken: null } : { email: address, phone: null, deviceToken: null }
|
|
3233
|
+
}));
|
|
3234
|
+
}
|
|
3235
|
+
return archivedResponse(deletedAt);
|
|
2907
3236
|
},
|
|
2908
3237
|
// Subject Access Request — the authenticated user's own data as a portable
|
|
2909
3238
|
// JSON bundle. Only auth-owned data, and only safe fields: no password hash,
|
|
@@ -2945,13 +3274,13 @@ function userController(store, config, bus) {
|
|
|
2945
3274
|
}
|
|
2946
3275
|
|
|
2947
3276
|
// src/controllers/oauth.controller.ts
|
|
2948
|
-
var
|
|
2949
|
-
var
|
|
3277
|
+
var import_node_crypto10 = require("crypto");
|
|
3278
|
+
var import_jsonwebtoken3 = __toESM(require("jsonwebtoken"), 1);
|
|
2950
3279
|
var import_core10 = require("@fonderie/core");
|
|
2951
3280
|
var import_events4 = require("@fonderie/events");
|
|
2952
3281
|
|
|
2953
3282
|
// src/services/jwks.ts
|
|
2954
|
-
var
|
|
3283
|
+
var import_node_crypto9 = require("crypto");
|
|
2955
3284
|
var TTL_MS = 60 * 60 * 1e3;
|
|
2956
3285
|
var MIN_REFETCH_MS = 60 * 1e3;
|
|
2957
3286
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
@@ -2985,7 +3314,7 @@ function createJwksSource(url) {
|
|
|
2985
3314
|
}
|
|
2986
3315
|
if (!jwk) return null;
|
|
2987
3316
|
try {
|
|
2988
|
-
return (0,
|
|
3317
|
+
return (0, import_node_crypto9.createPublicKey)({ key: jwk, format: "jwk" });
|
|
2989
3318
|
} catch {
|
|
2990
3319
|
return null;
|
|
2991
3320
|
}
|
|
@@ -3031,7 +3360,7 @@ var APPLE_KEYS_URL = "https://appleid.apple.com/auth/keys";
|
|
|
3031
3360
|
var APPLE_ISSUER = "https://appleid.apple.com";
|
|
3032
3361
|
function mintAppleClientSecret(apple) {
|
|
3033
3362
|
const now = Math.floor(Date.now() / 1e3);
|
|
3034
|
-
return
|
|
3363
|
+
return import_jsonwebtoken3.default.sign(
|
|
3035
3364
|
{ iss: apple.teamId, iat: now, exp: now + 300, aud: APPLE_ISSUER, sub: apple.clientId },
|
|
3036
3365
|
apple.privateKey,
|
|
3037
3366
|
{ algorithm: "ES256", keyid: apple.keyId }
|
|
@@ -3040,14 +3369,14 @@ function mintAppleClientSecret(apple) {
|
|
|
3040
3369
|
var appleKeys = createJwksSource(APPLE_KEYS_URL);
|
|
3041
3370
|
async function verifyAppleIdToken(idToken, opts) {
|
|
3042
3371
|
if (opts.audiences.length === 0) return null;
|
|
3043
|
-
const decoded =
|
|
3372
|
+
const decoded = import_jsonwebtoken3.default.decode(idToken, { complete: true });
|
|
3044
3373
|
if (!decoded || typeof decoded === "string") return null;
|
|
3045
3374
|
const kid = decoded.header.kid;
|
|
3046
3375
|
if (!kid) return null;
|
|
3047
3376
|
const key = await appleKeys.getKey(kid);
|
|
3048
3377
|
if (!key) return null;
|
|
3049
3378
|
try {
|
|
3050
|
-
const payload =
|
|
3379
|
+
const payload = import_jsonwebtoken3.default.verify(idToken, key, {
|
|
3051
3380
|
algorithms: ["RS256"],
|
|
3052
3381
|
issuer: APPLE_ISSUER,
|
|
3053
3382
|
// jsonwebtoken types `audience` as a non-empty tuple; runtime accepts a
|
|
@@ -3067,14 +3396,14 @@ var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
|
|
|
3067
3396
|
var googleKeys = createJwksSource(GOOGLE_KEYS_URL);
|
|
3068
3397
|
async function verifyGoogleIdToken(idToken, opts) {
|
|
3069
3398
|
if (opts.audiences.length === 0) return null;
|
|
3070
|
-
const decoded =
|
|
3399
|
+
const decoded = import_jsonwebtoken3.default.decode(idToken, { complete: true });
|
|
3071
3400
|
if (!decoded || typeof decoded === "string") return null;
|
|
3072
3401
|
const kid = decoded.header.kid;
|
|
3073
3402
|
if (!kid) return null;
|
|
3074
3403
|
const key = await googleKeys.getKey(kid);
|
|
3075
3404
|
if (!key) return null;
|
|
3076
3405
|
try {
|
|
3077
|
-
const payload =
|
|
3406
|
+
const payload = import_jsonwebtoken3.default.verify(idToken, key, {
|
|
3078
3407
|
algorithms: ["RS256"],
|
|
3079
3408
|
issuer: [...GOOGLE_ISSUERS],
|
|
3080
3409
|
audience: opts.audiences
|
|
@@ -3176,7 +3505,7 @@ function oauthController(store, config, bus) {
|
|
|
3176
3505
|
failureReason: "pending_deletion",
|
|
3177
3506
|
...meta
|
|
3178
3507
|
});
|
|
3179
|
-
const res = pendingDeletionResponse(archived.deletedAt, config);
|
|
3508
|
+
const res = pendingDeletionResponse({ ...archived, deletedAt: archived.deletedAt }, provider, config);
|
|
3180
3509
|
const headers = cookieHeaders(clearCookies);
|
|
3181
3510
|
headers.set("content-type", res.headers.get("content-type") ?? "application/json");
|
|
3182
3511
|
return new Response(res.body, { status: res.status, headers });
|
|
@@ -3248,7 +3577,7 @@ function oauthController(store, config, bus) {
|
|
|
3248
3577
|
"Google OAuth not configured"
|
|
3249
3578
|
);
|
|
3250
3579
|
}
|
|
3251
|
-
const state = (0,
|
|
3580
|
+
const state = (0, import_node_crypto10.randomBytes)(16).toString("hex");
|
|
3252
3581
|
const params = new URLSearchParams({
|
|
3253
3582
|
client_id: google.clientId,
|
|
3254
3583
|
redirect_uri: google.redirectUri,
|
|
@@ -3350,7 +3679,7 @@ function oauthController(store, config, bus) {
|
|
|
3350
3679
|
if (!apple) {
|
|
3351
3680
|
return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3352
3681
|
}
|
|
3353
|
-
const state = (0,
|
|
3682
|
+
const state = (0, import_node_crypto10.randomBytes)(16).toString("hex");
|
|
3354
3683
|
const params = new URLSearchParams({
|
|
3355
3684
|
client_id: apple.clientId,
|
|
3356
3685
|
redirect_uri: apple.redirectUri,
|
|
@@ -3446,7 +3775,7 @@ function oauthController(store, config, bus) {
|
|
|
3446
3775
|
});
|
|
3447
3776
|
return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3448
3777
|
}
|
|
3449
|
-
const tokenHash = (0,
|
|
3778
|
+
const tokenHash = (0, import_node_crypto10.createHash)("sha256").update(identityToken).digest("hex");
|
|
3450
3779
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
3451
3780
|
const firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));
|
|
3452
3781
|
if (!firstUse) {
|
|
@@ -3496,7 +3825,7 @@ function oauthController(store, config, bus) {
|
|
|
3496
3825
|
});
|
|
3497
3826
|
return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "Invalid Google ID token");
|
|
3498
3827
|
}
|
|
3499
|
-
const tokenHash = (0,
|
|
3828
|
+
const tokenHash = (0, import_node_crypto10.createHash)("sha256").update(idToken).digest("hex");
|
|
3500
3829
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
3501
3830
|
if (!await consumedTokens.consumeOnce(tokenHash, new Date(expMs))) {
|
|
3502
3831
|
await loginEvents.recordSafe({
|
|
@@ -3585,6 +3914,11 @@ function buildAuthRoutes(store, config, bus, locales) {
|
|
|
3585
3914
|
// refuses when it would leave the account with no way to sign in.
|
|
3586
3915
|
R("unlinkOauth", "DELETE", "/auth/oauth/:provider", import_middlewares2.requireAuth, user.unlinkOauth),
|
|
3587
3916
|
R("deleteMe", "DELETE", "/users", import_middlewares2.requireAuth, verifyGate, user.deleteMe),
|
|
3917
|
+
// Deleting with proof: a code to a chosen channel, then confirm. The
|
|
3918
|
+
// account is archived until the purge; signing in offers to keep it.
|
|
3919
|
+
R("requestDeletion", "POST", "/users/me/deletion", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(requestDeletionSchema), user.requestDeletion),
|
|
3920
|
+
R("confirmDeletion", "POST", "/users/me/deletion/confirm", ipLimit("verify"), import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(confirmDeletionSchema), user.confirmDeletion),
|
|
3921
|
+
R("restoreAccount", "POST", "/auth/account/restore", ipLimit("login"), (0, import_middlewares.validate)(restoreAccountSchema), auth.restoreAccount),
|
|
3588
3922
|
R("exportMe", "GET", "/users/export", import_middlewares2.requireAuth, user.exportMe),
|
|
3589
3923
|
// Security surfaces (Protected; the caller's own login history + sessions).
|
|
3590
3924
|
// The literal /others route is registered before /:id so it isn't
|
|
@@ -3788,8 +4122,11 @@ function withSession(store, config) {
|
|
|
3788
4122
|
if (payload.sid && !await sessions.aliveBySid(payload.sid)) {
|
|
3789
4123
|
return next();
|
|
3790
4124
|
}
|
|
3791
|
-
|
|
3792
|
-
if (!user
|
|
4125
|
+
let user = await users.findById(payload.sub);
|
|
4126
|
+
if (!user && payload.mfaPending && payload.loginMethod === "phone") {
|
|
4127
|
+
user = await users.findArchivedById(payload.sub);
|
|
4128
|
+
}
|
|
4129
|
+
if (!user || user.suspended || user.deletedAt && !payload.mfaPending) {
|
|
3793
4130
|
return next();
|
|
3794
4131
|
}
|
|
3795
4132
|
Object.assign(ctx, {
|
|
@@ -3964,7 +4301,7 @@ var AuthModule = class {
|
|
|
3964
4301
|
name = "@fonderie/auth";
|
|
3965
4302
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3966
4303
|
// say what is actually deployed rather than 'not reported'.
|
|
3967
|
-
version = "7.
|
|
4304
|
+
version = "7.24.0";
|
|
3968
4305
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3969
4306
|
describeAdmin() {
|
|
3970
4307
|
return { routes: describeAuthAdminRoutes(this.store, this.bus) };
|
|
@@ -4152,6 +4489,29 @@ inmediato y cambia tu contrase\xF1a \u2014 otra persona podr\xEDa iniciar sesi\x
|
|
|
4152
4489
|
{{provider}} ya no se puede usar para iniciar sesi\xF3n. Tu correo electr\xF3nico y tu contrase\xF1a siguen funcionando.
|
|
4153
4490
|
|
|
4154
4491
|
Si fuiste t\xFA, todo est\xE1 en orden. Si no, contacta con soporte de inmediato.`
|
|
4492
|
+
},
|
|
4493
|
+
[MESSAGE_KEYS.accountDeletionCode]: {
|
|
4494
|
+
subject: `Confirma la eliminaci\xF3n de tu cuenta`,
|
|
4495
|
+
html: `<h1>Confirmar la eliminaci\xF3n de la cuenta</h1>
|
|
4496
|
+
<p>Usa este c\xF3digo para confirmar que quieres eliminar tu cuenta:</p>
|
|
4497
|
+
<p><span class="pin-code">{{code}}</span></p>
|
|
4498
|
+
<p class="muted">Este c\xF3digo vence en 15 minutos. Si no pediste eliminar tu cuenta, ignora este mensaje y cambia tu contrase\xF1a: tu cuenta seguir\xE1 igual.</p>`,
|
|
4499
|
+
text: `Tu c\xF3digo para confirmar la eliminaci\xF3n de tu cuenta: {{code}}. Vence en 15 minutos. \xBFNo fuiste t\xFA? Ignora esto y cambia tu contrase\xF1a.`
|
|
4500
|
+
},
|
|
4501
|
+
[MESSAGE_KEYS.accountDeletionScheduled]: {
|
|
4502
|
+
subject: `Tu cuenta se eliminar\xE1 el {{deleteOn}}`,
|
|
4503
|
+
html: `<h1>Tu cuenta est\xE1 programada para eliminarse</h1>
|
|
4504
|
+
<p>Recibimos tu solicitud. Tu cuenta est\xE1 cerrada y se eliminar\xE1 de forma permanente el <strong>{{deleteOn}}</strong>.</p>
|
|
4505
|
+
<p>\xBFCambiaste de opini\xF3n? Inicia sesi\xF3n antes de esa fecha y elige <strong>Conservar mi cuenta</strong>.</p>
|
|
4506
|
+
<p class="muted">Si no lo pediste, inicia sesi\xF3n ahora para conservar tu cuenta y cambia tu contrase\xF1a.</p>`,
|
|
4507
|
+
text: `Tu cuenta se eliminar\xE1 de forma permanente el {{deleteOn}}. \xBFCambiaste de opini\xF3n? Inicia sesi\xF3n antes y elige Conservar mi cuenta.`
|
|
4508
|
+
},
|
|
4509
|
+
[MESSAGE_KEYS.accountRestored]: {
|
|
4510
|
+
subject: `Tu cuenta fue restablecida`,
|
|
4511
|
+
html: `<h1>Bienvenido de nuevo</h1>
|
|
4512
|
+
<p>Se cancel\xF3 la eliminaci\xF3n de tu cuenta y tu cuenta vuelve a estar activa.</p>
|
|
4513
|
+
<p class="muted">Si no fuiste t\xFA, cambia tu contrase\xF1a de inmediato.</p>`,
|
|
4514
|
+
text: `Se cancel\xF3 la eliminaci\xF3n de tu cuenta; tu cuenta vuelve a estar activa. \xBFNo fuiste t\xFA? Cambia tu contrase\xF1a ahora.`
|
|
4155
4515
|
}
|
|
4156
4516
|
};
|
|
4157
4517
|
|
|
@@ -4312,6 +4672,29 @@ pourrait se connecter \xE0 votre place.`
|
|
|
4312
4672
|
{{provider}} ne peut plus \xEAtre utilis\xE9 pour vous connecter. Votre adresse e-mail et votre mot de passe fonctionnent toujours.
|
|
4313
4673
|
|
|
4314
4674
|
Si c'est bien vous, tout est en ordre. Sinon, contactez imm\xE9diatement le support.`
|
|
4675
|
+
},
|
|
4676
|
+
[MESSAGE_KEYS.accountDeletionCode]: {
|
|
4677
|
+
subject: `Confirmez la suppression de votre compte`,
|
|
4678
|
+
html: `<h1>Confirmer la suppression du compte</h1>
|
|
4679
|
+
<p>Utilisez ce code pour confirmer que vous souhaitez supprimer votre compte :</p>
|
|
4680
|
+
<p><span class="pin-code">{{code}}</span></p>
|
|
4681
|
+
<p class="muted">Ce code expire dans 15 minutes. Si vous n’avez pas demand\xE9 la suppression de votre compte, ignorez ce message et changez votre mot de passe — votre compte reste tel quel.</p>`,
|
|
4682
|
+
text: `Votre code pour confirmer la suppression de votre compte : {{code}}. Il expire dans 15 minutes. Ce n'est pas vous ? Ignorez ce message et changez votre mot de passe.`
|
|
4683
|
+
},
|
|
4684
|
+
[MESSAGE_KEYS.accountDeletionScheduled]: {
|
|
4685
|
+
subject: `Votre compte sera supprim\xE9 le {{deleteOn}}`,
|
|
4686
|
+
html: `<h1>Votre compte sera supprim\xE9</h1>
|
|
4687
|
+
<p>Nous avons bien re\xE7u votre demande. Votre compte est ferm\xE9 et sera supprim\xE9 d\xE9finitivement le <strong>{{deleteOn}}</strong>.</p>
|
|
4688
|
+
<p>Vous avez chang\xE9 d’avis ? Connectez-vous avant cette date et choisissez <strong>Conserver mon compte</strong>.</p>
|
|
4689
|
+
<p class="muted">Si vous n’\xEAtes pas \xE0 l’origine de cette demande, connectez-vous maintenant pour conserver votre compte et changez votre mot de passe.</p>`,
|
|
4690
|
+
text: `Votre compte sera supprim\xE9 d\xE9finitivement le {{deleteOn}}. Vous avez chang\xE9 d'avis ? Connectez-vous avant cette date et choisissez Conserver mon compte.`
|
|
4691
|
+
},
|
|
4692
|
+
[MESSAGE_KEYS.accountRestored]: {
|
|
4693
|
+
subject: `Votre compte a \xE9t\xE9 r\xE9tabli`,
|
|
4694
|
+
html: `<h1>Bon retour</h1>
|
|
4695
|
+
<p>La suppression de votre compte a \xE9t\xE9 annul\xE9e et votre compte est de nouveau actif.</p>
|
|
4696
|
+
<p class="muted">Si ce n’est pas vous, changez votre mot de passe imm\xE9diatement.</p>`,
|
|
4697
|
+
text: `La suppression de votre compte a \xE9t\xE9 annul\xE9e ; votre compte est de nouveau actif. Ce n'est pas vous ? Changez votre mot de passe maintenant.`
|
|
4315
4698
|
}
|
|
4316
4699
|
};
|
|
4317
4700
|
|
|
@@ -4466,6 +4849,29 @@ var ZH_HANS_TEMPLATES = {
|
|
|
4466
4849
|
\u5DF2\u65E0\u6CD5\u518D\u4F7F\u7528 {{provider}} \u767B\u5F55\u3002\u60A8\u7684\u7535\u5B50\u90AE\u7BB1\u548C\u5BC6\u7801\u4ECD\u7136\u53EF\u7528\u3002
|
|
4467
4850
|
|
|
4468
4851
|
\u5982\u679C\u662F\u60A8\u672C\u4EBA\u79FB\u9664\u7684\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\u3002`
|
|
4852
|
+
},
|
|
4853
|
+
[MESSAGE_KEYS.accountDeletionCode]: {
|
|
4854
|
+
subject: `\u786E\u8BA4\u5220\u9664\u60A8\u7684\u8D26\u6237`,
|
|
4855
|
+
html: `<h1>\u786E\u8BA4\u5220\u9664\u8D26\u6237</h1>
|
|
4856
|
+
<p>\u8BF7\u4F7F\u7528\u6B64\u9A8C\u8BC1\u7801\u786E\u8BA4\u60A8\u8981\u5220\u9664\u8D26\u6237\uFF1A</p>
|
|
4857
|
+
<p><span class="pin-code">{{code}}</span></p>
|
|
4858
|
+
<p class="muted">\u6B64\u9A8C\u8BC1\u7801\u5C06\u5728 15 \u5206\u949F\u540E\u5931\u6548\u3002\u5982\u679C\u60A8\u6CA1\u6709\u7533\u8BF7\u5220\u9664\u8D26\u6237\uFF0C\u8BF7\u5FFD\u7565\u6B64\u6D88\u606F\u5E76\u66F4\u6539\u5BC6\u7801\uFF0C\u60A8\u7684\u8D26\u6237\u5C06\u4FDD\u6301\u4E0D\u53D8\u3002</p>`,
|
|
4859
|
+
text: `\u786E\u8BA4\u5220\u9664\u8D26\u6237\u7684\u9A8C\u8BC1\u7801\uFF1A{{code}}\uFF0C15 \u5206\u949F\u5185\u6709\u6548\u3002\u4E0D\u662F\u60A8\u672C\u4EBA\u64CD\u4F5C\uFF1F\u8BF7\u5FFD\u7565\u5E76\u66F4\u6539\u5BC6\u7801\u3002`
|
|
4860
|
+
},
|
|
4861
|
+
[MESSAGE_KEYS.accountDeletionScheduled]: {
|
|
4862
|
+
subject: `\u60A8\u7684\u8D26\u6237\u5C06\u4E8E {{deleteOn}} \u5220\u9664`,
|
|
4863
|
+
html: `<h1>\u60A8\u7684\u8D26\u6237\u5DF2\u5B89\u6392\u5220\u9664</h1>
|
|
4864
|
+
<p>\u6211\u4EEC\u5DF2\u6536\u5230\u60A8\u7684\u7533\u8BF7\u3002\u60A8\u7684\u8D26\u6237\u5DF2\u5173\u95ED\uFF0C\u5E76\u5C06\u4E8E <strong>{{deleteOn}}</strong> \u6C38\u4E45\u5220\u9664\u3002</p>
|
|
4865
|
+
<p>\u6539\u53D8\u4E3B\u610F\u4E86\uFF1F\u8BF7\u5728\u8BE5\u65E5\u671F\u524D\u767B\u5F55\u5E76\u9009\u62E9<strong>\u4FDD\u7559\u6211\u7684\u8D26\u6237</strong>\u3002</p>
|
|
4866
|
+
<p class="muted">\u5982\u679C\u8FD9\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u7533\u8BF7\uFF0C\u8BF7\u7ACB\u5373\u767B\u5F55\u4EE5\u4FDD\u7559\u8D26\u6237\u5E76\u66F4\u6539\u5BC6\u7801\u3002</p>`,
|
|
4867
|
+
text: `\u60A8\u7684\u8D26\u6237\u5C06\u4E8E {{deleteOn}} \u6C38\u4E45\u5220\u9664\u3002\u6539\u53D8\u4E3B\u610F\u4E86\uFF1F\u8BF7\u5728\u6B64\u524D\u767B\u5F55\u5E76\u9009\u62E9\u201C\u4FDD\u7559\u6211\u7684\u8D26\u6237\u201D\u3002`
|
|
4868
|
+
},
|
|
4869
|
+
[MESSAGE_KEYS.accountRestored]: {
|
|
4870
|
+
subject: `\u60A8\u7684\u8D26\u6237\u5DF2\u6062\u590D`,
|
|
4871
|
+
html: `<h1>\u6B22\u8FCE\u56DE\u6765</h1>
|
|
4872
|
+
<p>\u60A8\u7684\u8D26\u6237\u5220\u9664\u5DF2\u53D6\u6D88\uFF0C\u8D26\u6237\u5DF2\u91CD\u65B0\u542F\u7528\u3002</p>
|
|
4873
|
+
<p class="muted">\u5982\u679C\u4E0D\u662F\u60A8\u672C\u4EBA\u64CD\u4F5C\uFF0C\u8BF7\u7ACB\u5373\u66F4\u6539\u5BC6\u7801\u3002</p>`,
|
|
4874
|
+
text: `\u60A8\u7684\u8D26\u6237\u5220\u9664\u5DF2\u53D6\u6D88\uFF0C\u8D26\u6237\u5DF2\u91CD\u65B0\u542F\u7528\u3002\u4E0D\u662F\u60A8\u672C\u4EBA\u64CD\u4F5C\uFF1F\u8BF7\u7ACB\u5373\u66F4\u6539\u5BC6\u7801\u3002`
|
|
4469
4875
|
}
|
|
4470
4876
|
};
|
|
4471
4877
|
|
|
@@ -4620,6 +5026,29 @@ var ZH_HANT_TEMPLATES = {
|
|
|
4620
5026
|
\u5DF2\u7121\u6CD5\u518D\u4F7F\u7528 {{provider}} \u767B\u5165\u3002\u60A8\u7684\u96FB\u5B50\u90F5\u4EF6\u548C\u5BC6\u78BC\u4ECD\u53EF\u4F7F\u7528\u3002
|
|
4621
5027
|
|
|
4622
5028
|
\u5982\u679C\u662F\u60A8\u672C\u4EBA\u79FB\u9664\u7684\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\u3002`
|
|
5029
|
+
},
|
|
5030
|
+
[MESSAGE_KEYS.accountDeletionCode]: {
|
|
5031
|
+
subject: `\u78BA\u8A8D\u522A\u9664\u60A8\u7684\u5E33\u6236`,
|
|
5032
|
+
html: `<h1>\u78BA\u8A8D\u522A\u9664\u5E33\u6236</h1>
|
|
5033
|
+
<p>\u8ACB\u4F7F\u7528\u6B64\u9A57\u8B49\u78BC\u78BA\u8A8D\u60A8\u8981\u522A\u9664\u5E33\u6236\uFF1A</p>
|
|
5034
|
+
<p><span class="pin-code">{{code}}</span></p>
|
|
5035
|
+
<p class="muted">\u6B64\u9A57\u8B49\u78BC\u5C07\u5728 15 \u5206\u9418\u5F8C\u5931\u6548\u3002\u5982\u679C\u60A8\u6C92\u6709\u7533\u8ACB\u522A\u9664\u5E33\u6236\uFF0C\u8ACB\u5FFD\u7565\u6B64\u8A0A\u606F\u4E26\u66F4\u6539\u5BC6\u78BC\uFF0C\u60A8\u7684\u5E33\u6236\u5C07\u4FDD\u6301\u4E0D\u8B8A\u3002</p>`,
|
|
5036
|
+
text: `\u78BA\u8A8D\u522A\u9664\u5E33\u6236\u7684\u9A57\u8B49\u78BC\uFF1A{{code}}\uFF0C15 \u5206\u9418\u5167\u6709\u6548\u3002\u4E0D\u662F\u60A8\u672C\u4EBA\u64CD\u4F5C\uFF1F\u8ACB\u5FFD\u7565\u4E26\u66F4\u6539\u5BC6\u78BC\u3002`
|
|
5037
|
+
},
|
|
5038
|
+
[MESSAGE_KEYS.accountDeletionScheduled]: {
|
|
5039
|
+
subject: `\u60A8\u7684\u5E33\u6236\u5C07\u65BC {{deleteOn}} \u522A\u9664`,
|
|
5040
|
+
html: `<h1>\u60A8\u7684\u5E33\u6236\u5DF2\u6392\u5B9A\u522A\u9664</h1>
|
|
5041
|
+
<p>\u6211\u5011\u5DF2\u6536\u5230\u60A8\u7684\u7533\u8ACB\u3002\u60A8\u7684\u5E33\u6236\u5DF2\u95DC\u9589\uFF0C\u4E26\u5C07\u65BC <strong>{{deleteOn}}</strong> \u6C38\u4E45\u522A\u9664\u3002</p>
|
|
5042
|
+
<p>\u6539\u8B8A\u4E3B\u610F\u4E86\uFF1F\u8ACB\u5728\u8A72\u65E5\u671F\u524D\u767B\u5165\u4E26\u9078\u64C7<strong>\u4FDD\u7559\u6211\u7684\u5E33\u6236</strong>\u3002</p>
|
|
5043
|
+
<p class="muted">\u5982\u679C\u9019\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u7533\u8ACB\uFF0C\u8ACB\u7ACB\u5373\u767B\u5165\u4EE5\u4FDD\u7559\u5E33\u6236\u4E26\u66F4\u6539\u5BC6\u78BC\u3002</p>`,
|
|
5044
|
+
text: `\u60A8\u7684\u5E33\u6236\u5C07\u65BC {{deleteOn}} \u6C38\u4E45\u522A\u9664\u3002\u6539\u8B8A\u4E3B\u610F\u4E86\uFF1F\u8ACB\u5728\u6B64\u524D\u767B\u5165\u4E26\u9078\u64C7\u300C\u4FDD\u7559\u6211\u7684\u5E33\u6236\u300D\u3002`
|
|
5045
|
+
},
|
|
5046
|
+
[MESSAGE_KEYS.accountRestored]: {
|
|
5047
|
+
subject: `\u60A8\u7684\u5E33\u6236\u5DF2\u6062\u5FA9`,
|
|
5048
|
+
html: `<h1>\u6B61\u8FCE\u56DE\u4F86</h1>
|
|
5049
|
+
<p>\u60A8\u7684\u5E33\u6236\u522A\u9664\u5DF2\u53D6\u6D88\uFF0C\u5E33\u6236\u5DF2\u91CD\u65B0\u555F\u7528\u3002</p>
|
|
5050
|
+
<p class="muted">\u5982\u679C\u4E0D\u662F\u60A8\u672C\u4EBA\u64CD\u4F5C\uFF0C\u8ACB\u7ACB\u5373\u66F4\u6539\u5BC6\u78BC\u3002</p>`,
|
|
5051
|
+
text: `\u60A8\u7684\u5E33\u6236\u522A\u9664\u5DF2\u53D6\u6D88\uFF0C\u5E33\u6236\u5DF2\u91CD\u65B0\u555F\u7528\u3002\u4E0D\u662F\u60A8\u672C\u4EBA\u64CD\u4F5C\uFF1F\u8ACB\u7ACB\u5373\u66F4\u6539\u5BC6\u78BC\u3002`
|
|
4623
5052
|
}
|
|
4624
5053
|
};
|
|
4625
5054
|
|
|
@@ -4778,6 +5207,29 @@ change your password \u2014 someone else may be able to sign in as you.`
|
|
|
4778
5207
|
{{provider}} can no longer be used to sign in. Your email and password still work.
|
|
4779
5208
|
|
|
4780
5209
|
If you did this, you're all set. If not, contact support right away.`
|
|
5210
|
+
},
|
|
5211
|
+
[MESSAGE_KEYS.accountDeletionCode]: {
|
|
5212
|
+
subject: `Confirm your account deletion`,
|
|
5213
|
+
html: `<h1>Confirm account deletion</h1>
|
|
5214
|
+
<p>Use this code to confirm that you want to delete your account:</p>
|
|
5215
|
+
<p><span class="pin-code">{{code}}</span></p>
|
|
5216
|
+
<p class="muted">This code expires in 15 minutes. If you didn’t ask to delete your account, ignore this message and change your password — your account stays as it is.</p>`,
|
|
5217
|
+
text: `Your code to confirm deleting your account: {{code}}. It expires in 15 minutes. Didn't ask? Ignore this and change your password.`
|
|
5218
|
+
},
|
|
5219
|
+
[MESSAGE_KEYS.accountDeletionScheduled]: {
|
|
5220
|
+
subject: `Your account will be deleted on {{deleteOn}}`,
|
|
5221
|
+
html: `<h1>Your account is scheduled for deletion</h1>
|
|
5222
|
+
<p>We received your request. Your account is closed and will be permanently deleted on <strong>{{deleteOn}}</strong>.</p>
|
|
5223
|
+
<p>Changed your mind? Sign in before that date and choose <strong>Keep my account</strong>.</p>
|
|
5224
|
+
<p class="muted">If you didn’t ask for this, sign in now to keep your account and change your password.</p>`,
|
|
5225
|
+
text: `Your account will be permanently deleted on {{deleteOn}}. Changed your mind? Sign in before then and choose Keep my account.`
|
|
5226
|
+
},
|
|
5227
|
+
[MESSAGE_KEYS.accountRestored]: {
|
|
5228
|
+
subject: `Your account was restored`,
|
|
5229
|
+
html: `<h1>Welcome back</h1>
|
|
5230
|
+
<p>Your account deletion was cancelled and your account is active again.</p>
|
|
5231
|
+
<p class="muted">If you didn’t do this, change your password right away.</p>`,
|
|
5232
|
+
text: `Your account deletion was cancelled; your account is active again. Didn't do this? Change your password now.`
|
|
4781
5233
|
}
|
|
4782
5234
|
};
|
|
4783
5235
|
var DEFAULT_TEMPLATES = (0, import_core15.withTranslations)(EN_TEMPLATES, {
|
|
@@ -4803,7 +5255,10 @@ var SAMPLE_PAYLOADS = {
|
|
|
4803
5255
|
[MESSAGE_KEYS.mfaDisabled]: {},
|
|
4804
5256
|
[MESSAGE_KEYS.mfaBackupCodesRegenerated]: {},
|
|
4805
5257
|
[MESSAGE_KEYS.emailChanged]: { newEmail: "new@example.com" },
|
|
4806
|
-
[MESSAGE_KEYS.phoneChanged]: {}
|
|
5258
|
+
[MESSAGE_KEYS.phoneChanged]: {},
|
|
5259
|
+
[MESSAGE_KEYS.accountDeletionCode]: { code: "123456" },
|
|
5260
|
+
[MESSAGE_KEYS.accountDeletionScheduled]: { deleteOn: "November 3, 2026" },
|
|
5261
|
+
[MESSAGE_KEYS.accountRestored]: {}
|
|
4807
5262
|
};
|
|
4808
5263
|
|
|
4809
5264
|
// src/middlewares/require-auth.ts
|