@fonderie/auth 7.22.0 → 7.23.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 +1 -1
- package/brain/signatures.md +3 -0
- package/dist/index.cjs +362 -261
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +320 -219
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +21 -0
- 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 +21 -0
- package/dist/middlewares/index.js.map +1 -1
- package/dist/{session-C1A21bvb.d.cts → session-DZ7QOw1J.d.cts} +3 -0
- package/dist/{session-C1A21bvb.d.ts → session-DZ7QOw1J.d.ts} +3 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -100,7 +100,7 @@ __export(index_exports, {
|
|
|
100
100
|
module.exports = __toCommonJS(index_exports);
|
|
101
101
|
|
|
102
102
|
// src/routes.ts
|
|
103
|
-
var
|
|
103
|
+
var import_core11 = require("@fonderie/core");
|
|
104
104
|
var import_middlewares2 = require("@fonderie/core/middlewares");
|
|
105
105
|
|
|
106
106
|
// src/middlewares/require-email-login.ts
|
|
@@ -125,6 +125,41 @@ var import_middlewares = require("@fonderie/core/middlewares");
|
|
|
125
125
|
|
|
126
126
|
// src/services/rate-limit.ts
|
|
127
127
|
var import_rate_limit = require("@fonderie/rate-limit");
|
|
128
|
+
|
|
129
|
+
// src/services/email.ts
|
|
130
|
+
function normalizeEmail(email2) {
|
|
131
|
+
if (typeof email2 !== "string" || email2.length === 0) {
|
|
132
|
+
throw new Error("Invalid email");
|
|
133
|
+
}
|
|
134
|
+
const lower = email2.trim().toLowerCase();
|
|
135
|
+
if (lower.length === 0) {
|
|
136
|
+
throw new Error("Email cannot be empty");
|
|
137
|
+
}
|
|
138
|
+
const atIndex = lower.indexOf("@");
|
|
139
|
+
if (atIndex === -1 || lower.lastIndexOf("@") !== atIndex) {
|
|
140
|
+
throw new Error("Invalid email format");
|
|
141
|
+
}
|
|
142
|
+
const local = lower.substring(0, atIndex);
|
|
143
|
+
const domain = lower.substring(atIndex + 1);
|
|
144
|
+
if (!local || !domain) {
|
|
145
|
+
throw new Error("Invalid email format");
|
|
146
|
+
}
|
|
147
|
+
const plusIndex = local.indexOf("+");
|
|
148
|
+
const normalizedLocal = plusIndex === -1 ? local : local.substring(0, plusIndex);
|
|
149
|
+
if (!normalizedLocal) {
|
|
150
|
+
throw new Error("Invalid email format");
|
|
151
|
+
}
|
|
152
|
+
return `${normalizedLocal}@${domain}`;
|
|
153
|
+
}
|
|
154
|
+
function normalizeEmailSafe(email2) {
|
|
155
|
+
try {
|
|
156
|
+
return normalizeEmail(email2);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/services/rate-limit.ts
|
|
128
163
|
var min = (n) => n * 60;
|
|
129
164
|
var DEFAULTS = {
|
|
130
165
|
// login: 10/15min per IP AND 5/15min per account (credential stuffing
|
|
@@ -177,7 +212,9 @@ function buildAuthAccountLimiter(route, store, config) {
|
|
|
177
212
|
return (0, import_rate_limit.rateLimit)({
|
|
178
213
|
store: r.store,
|
|
179
214
|
rule: r.account.rule,
|
|
180
|
-
|
|
215
|
+
// The account bucket is keyed the way accounts are stored, so a rotating
|
|
216
|
+
// '+tag' on the same address does not buy fresh attempts.
|
|
217
|
+
key: (0, import_rate_limit.byBodyField)(`auth:${route}`, r.account.field, r.account.field === "email" ? normalizeEmailSafe : void 0)
|
|
181
218
|
});
|
|
182
219
|
}
|
|
183
220
|
|
|
@@ -352,6 +389,10 @@ var EVENT_KEYS = {
|
|
|
352
389
|
// 'password-changed', 'admin', 'refresh-reuse' (theft signal).
|
|
353
390
|
sessionRevoked: "fonderie.session.revoked"
|
|
354
391
|
};
|
|
392
|
+
var deletionGracePeriodDays = (config) => {
|
|
393
|
+
const days = config.accountDeletion?.gracePeriodDays;
|
|
394
|
+
return typeof days === "number" && Number.isFinite(days) && days >= 0 ? days : 30;
|
|
395
|
+
};
|
|
355
396
|
|
|
356
397
|
// src/services/jwt.ts
|
|
357
398
|
var import_node_crypto = require("crypto");
|
|
@@ -768,6 +809,27 @@ var UserModel = class {
|
|
|
768
809
|
);
|
|
769
810
|
return row ?? null;
|
|
770
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* An ARCHIVED (soft-deleted, not yet purged) account holding this address.
|
|
814
|
+
* Only for telling its owner — after they prove it is theirs — that it is
|
|
815
|
+
* scheduled for deletion, and for refusing to create a second account on
|
|
816
|
+
* the address meanwhile. Never for signing in.
|
|
817
|
+
*/
|
|
818
|
+
async findArchivedByEmail(email2) {
|
|
819
|
+
const [row] = await this.store.query(
|
|
820
|
+
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NOT NULL`,
|
|
821
|
+
[email2]
|
|
822
|
+
);
|
|
823
|
+
return row ?? null;
|
|
824
|
+
}
|
|
825
|
+
/** As findArchivedByEmail, for a phone number. */
|
|
826
|
+
async findArchivedByPhone(phone2) {
|
|
827
|
+
const [row] = await this.store.query(
|
|
828
|
+
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NOT NULL`,
|
|
829
|
+
[phone2]
|
|
830
|
+
);
|
|
831
|
+
return row ?? null;
|
|
832
|
+
}
|
|
771
833
|
async findByPhone(phone2) {
|
|
772
834
|
const [row] = await this.store.query(
|
|
773
835
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
|
|
@@ -1622,7 +1684,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1622
1684
|
// src/controllers/auth.controller.ts
|
|
1623
1685
|
var import_node_crypto6 = require("crypto");
|
|
1624
1686
|
var import_events2 = require("@fonderie/events");
|
|
1625
|
-
var
|
|
1687
|
+
var import_core7 = require("@fonderie/core");
|
|
1626
1688
|
|
|
1627
1689
|
// src/services/cooldown.ts
|
|
1628
1690
|
function checkCooldown(lastSentAt, cooldownMs) {
|
|
@@ -1633,37 +1695,26 @@ function checkCooldown(lastSentAt, cooldownMs) {
|
|
|
1633
1695
|
// src/controllers/auth.controller.ts
|
|
1634
1696
|
init_password();
|
|
1635
1697
|
|
|
1636
|
-
// src/services/
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
}
|
|
1641
|
-
const lower = email2.trim().toLowerCase();
|
|
1642
|
-
if (lower.length === 0) {
|
|
1643
|
-
throw new Error("Email cannot be empty");
|
|
1644
|
-
}
|
|
1645
|
-
const atIndex = lower.indexOf("@");
|
|
1646
|
-
if (atIndex === -1 || lower.lastIndexOf("@") !== atIndex) {
|
|
1647
|
-
throw new Error("Invalid email format");
|
|
1648
|
-
}
|
|
1649
|
-
const local = lower.substring(0, atIndex);
|
|
1650
|
-
const domain = lower.substring(atIndex + 1);
|
|
1651
|
-
if (!local || !domain) {
|
|
1652
|
-
throw new Error("Invalid email format");
|
|
1653
|
-
}
|
|
1654
|
-
const plusIndex = local.indexOf("+");
|
|
1655
|
-
const normalizedLocal = plusIndex === -1 ? local : local.substring(0, plusIndex);
|
|
1656
|
-
if (!normalizedLocal) {
|
|
1657
|
-
throw new Error("Invalid email format");
|
|
1658
|
-
}
|
|
1659
|
-
return `${normalizedLocal}@${domain}`;
|
|
1698
|
+
// src/services/pending-deletion.ts
|
|
1699
|
+
var import_core6 = require("@fonderie/core");
|
|
1700
|
+
function deletionDate(deletedAt, config) {
|
|
1701
|
+
return new Date(deletedAt.getTime() + deletionGracePeriodDays(config) * 24 * 60 * 60 * 1e3);
|
|
1660
1702
|
}
|
|
1661
|
-
function
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1703
|
+
function pendingDeletionResponse(deletedAt, config) {
|
|
1704
|
+
const deleteOn = deletionDate(deletedAt, config);
|
|
1705
|
+
return (0, import_core6.setApiResponse)(
|
|
1706
|
+
import_core6.HTTP.FORBIDDEN,
|
|
1707
|
+
"ACCOUNT_PENDING_DELETION",
|
|
1708
|
+
`This account is scheduled for deletion on ${deleteOn.toISOString().slice(0, 10)}.`,
|
|
1709
|
+
{ requestedAt: deletedAt.toISOString(), deleteOn: deleteOn.toISOString() }
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
function archivedAddressResponse() {
|
|
1713
|
+
return (0, import_core6.setApiResponse)(
|
|
1714
|
+
import_core6.HTTP.CONFLICT,
|
|
1715
|
+
"ACCOUNT_PENDING_DELETION",
|
|
1716
|
+
"An account using this address is scheduled for deletion. Sign in to keep it, or wait until it is deleted."
|
|
1717
|
+
);
|
|
1667
1718
|
}
|
|
1668
1719
|
|
|
1669
1720
|
// src/models/password-reset.model.ts
|
|
@@ -1822,7 +1873,7 @@ function extractRefreshToken(ctx) {
|
|
|
1822
1873
|
const match = cookie.match(/(?:^|;\s*)refresh_token=([^;]+)/);
|
|
1823
1874
|
return match?.[1] ?? null;
|
|
1824
1875
|
}
|
|
1825
|
-
function authController(store, config, bus, locales = (0,
|
|
1876
|
+
function authController(store, config, bus, locales = (0, import_core7.defineLocales)()) {
|
|
1826
1877
|
const users = new UserModel(store);
|
|
1827
1878
|
const sessions = new SessionModel(store, config.location);
|
|
1828
1879
|
const loginEvents = new LoginEventModel(store, config.location);
|
|
@@ -1834,23 +1885,24 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1834
1885
|
register: async (ctx) => {
|
|
1835
1886
|
const body = ctx.meta["body"];
|
|
1836
1887
|
const { email: email2, password: password2, phone: phone2, firstName = null, lastName = null } = body ?? {};
|
|
1837
|
-
const locale = (0,
|
|
1888
|
+
const locale = (0, import_core7.canonicalLocale)(body?.["locale"]) ?? locales.default;
|
|
1838
1889
|
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
1839
1890
|
const normalizedEmail = normalizeEmailSafe(email2);
|
|
1840
1891
|
if (!normalizedEmail) {
|
|
1841
|
-
return (0,
|
|
1892
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1842
1893
|
}
|
|
1843
1894
|
if (password2.length < 8) {
|
|
1844
|
-
return (0,
|
|
1845
|
-
|
|
1895
|
+
return (0, import_core7.setApiResponse)(
|
|
1896
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
1846
1897
|
"INVALID_PARAMETER",
|
|
1847
1898
|
"password must be at least 8 characters"
|
|
1848
1899
|
);
|
|
1849
1900
|
}
|
|
1850
1901
|
const existing = await users.findByEmail(normalizedEmail);
|
|
1851
1902
|
if (existing) {
|
|
1852
|
-
return (0,
|
|
1903
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
1853
1904
|
}
|
|
1905
|
+
if (await users.findArchivedByEmail(normalizedEmail)) return archivedAddressResponse();
|
|
1854
1906
|
const passwordHash = await hashPassword(password2);
|
|
1855
1907
|
const row = await users.create(
|
|
1856
1908
|
normalizedEmail,
|
|
@@ -1860,18 +1912,18 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1860
1912
|
locale
|
|
1861
1913
|
);
|
|
1862
1914
|
if (!row) {
|
|
1863
|
-
return (0,
|
|
1915
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1864
1916
|
}
|
|
1865
1917
|
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
1866
1918
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1867
1919
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1868
1920
|
const user = await users.findById(row.id);
|
|
1869
1921
|
if (!user) {
|
|
1870
|
-
return (0,
|
|
1922
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1871
1923
|
}
|
|
1872
1924
|
const reqId = ctx.meta["requestId"];
|
|
1873
1925
|
const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
|
|
1874
|
-
await (0,
|
|
1926
|
+
await (0, import_core7.background)(bus?.emit(
|
|
1875
1927
|
import_events2.NOTIFICATION_EVENT,
|
|
1876
1928
|
{
|
|
1877
1929
|
type: MESSAGE_KEYS.emailRegistration,
|
|
@@ -1881,7 +1933,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1881
1933
|
},
|
|
1882
1934
|
reqOpts
|
|
1883
1935
|
));
|
|
1884
|
-
await (0,
|
|
1936
|
+
await (0, import_core7.background)(bus?.emit(
|
|
1885
1937
|
EVENT_KEYS.userRegistered,
|
|
1886
1938
|
{
|
|
1887
1939
|
userId: user.id,
|
|
@@ -1925,8 +1977,9 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1925
1977
|
if (isValidPhone(phone2)) {
|
|
1926
1978
|
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
1927
1979
|
if (existing) {
|
|
1928
|
-
return (0,
|
|
1980
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
1929
1981
|
}
|
|
1982
|
+
if (await users.findArchivedByPhone(normalizePhone(phone2))) return archivedAddressResponse();
|
|
1930
1983
|
const { id } = await users.findOrCreateByPhone(
|
|
1931
1984
|
normalizePhone(phone2),
|
|
1932
1985
|
firstName ?? null,
|
|
@@ -1938,11 +1991,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1938
1991
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1939
1992
|
const user = await users.findById(id);
|
|
1940
1993
|
if (!user) {
|
|
1941
|
-
return (0,
|
|
1994
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1942
1995
|
}
|
|
1943
1996
|
const reqId2 = ctx.meta["requestId"];
|
|
1944
1997
|
const reqOpts2 = reqId2 !== void 0 ? { requestId: reqId2 } : void 0;
|
|
1945
|
-
await (0,
|
|
1998
|
+
await (0, import_core7.background)(bus?.emit(
|
|
1946
1999
|
import_events2.NOTIFICATION_EVENT,
|
|
1947
2000
|
{
|
|
1948
2001
|
type: MESSAGE_KEYS.phoneOtp,
|
|
@@ -1952,7 +2005,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1952
2005
|
},
|
|
1953
2006
|
reqOpts2
|
|
1954
2007
|
));
|
|
1955
|
-
await (0,
|
|
2008
|
+
await (0, import_core7.background)(bus?.emit(
|
|
1956
2009
|
EVENT_KEYS.userRegistered,
|
|
1957
2010
|
{
|
|
1958
2011
|
userId: user.id,
|
|
@@ -1976,8 +2029,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1976
2029
|
{ status: 202 }
|
|
1977
2030
|
);
|
|
1978
2031
|
}
|
|
1979
|
-
return (0,
|
|
1980
|
-
|
|
2032
|
+
return (0, import_core7.setApiResponse)(
|
|
2033
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
1981
2034
|
"INVALID_PARAMETER",
|
|
1982
2035
|
"Provide email + password or a valid phone number"
|
|
1983
2036
|
);
|
|
@@ -1989,9 +2042,25 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1989
2042
|
const { email: rawEmail, password: password2 } = body;
|
|
1990
2043
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
1991
2044
|
if (!email2) {
|
|
1992
|
-
return (0,
|
|
2045
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1993
2046
|
}
|
|
1994
2047
|
const user = await users.findByEmail(email2);
|
|
2048
|
+
if (!user) {
|
|
2049
|
+
const archived = await users.findArchivedByEmail(email2);
|
|
2050
|
+
if (archived?.passwordHash && archived.deletedAt) {
|
|
2051
|
+
const { valid: valid2 } = await verifyPasswordForLogin(password2, archived.passwordHash, config.legacyVerify);
|
|
2052
|
+
await loginEvents.recordSafe({
|
|
2053
|
+
userId: archived.id,
|
|
2054
|
+
emailAttempted: email2,
|
|
2055
|
+
method: "password",
|
|
2056
|
+
outcome: "failed",
|
|
2057
|
+
failureReason: valid2 ? "pending_deletion" : "bad_password",
|
|
2058
|
+
...meta
|
|
2059
|
+
});
|
|
2060
|
+
if (valid2) return pendingDeletionResponse(archived.deletedAt, config);
|
|
2061
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
1995
2064
|
if (!user || !user.passwordHash) {
|
|
1996
2065
|
await loginEvents.recordSafe({
|
|
1997
2066
|
userId: null,
|
|
@@ -2001,7 +2070,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2001
2070
|
failureReason: "unknown_email",
|
|
2002
2071
|
...meta
|
|
2003
2072
|
});
|
|
2004
|
-
return (0,
|
|
2073
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2005
2074
|
}
|
|
2006
2075
|
const { valid, needsRehash } = await verifyPasswordForLogin(
|
|
2007
2076
|
password2,
|
|
@@ -2017,21 +2086,21 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2017
2086
|
failureReason: "bad_password",
|
|
2018
2087
|
...meta
|
|
2019
2088
|
});
|
|
2020
|
-
return (0,
|
|
2089
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2021
2090
|
}
|
|
2022
2091
|
if (needsRehash) {
|
|
2023
2092
|
await users.updatePassword(user.id, await hashPassword(password2));
|
|
2024
2093
|
}
|
|
2025
2094
|
if (user.suspended) {
|
|
2026
|
-
return (0,
|
|
2027
|
-
|
|
2095
|
+
return (0, import_core7.setApiResponse)(
|
|
2096
|
+
import_core7.HTTP.FORBIDDEN,
|
|
2028
2097
|
"ACCOUNT_SUSPENDED",
|
|
2029
2098
|
"Account suspended. Please contact support."
|
|
2030
2099
|
);
|
|
2031
2100
|
}
|
|
2032
2101
|
if (user.mfaEnabled) {
|
|
2033
2102
|
const mfaToken = issueMfaPendingToken(user.id, config, "email");
|
|
2034
|
-
return (0,
|
|
2103
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
|
|
2035
2104
|
mfaToken
|
|
2036
2105
|
});
|
|
2037
2106
|
}
|
|
@@ -2068,11 +2137,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2068
2137
|
if (isValidPhone(phone2)) {
|
|
2069
2138
|
const user = await users.findByPhone(normalizePhone(phone2));
|
|
2070
2139
|
if (!user) {
|
|
2071
|
-
return (0,
|
|
2140
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2072
2141
|
}
|
|
2073
2142
|
if (user.suspended) {
|
|
2074
|
-
return (0,
|
|
2075
|
-
|
|
2143
|
+
return (0, import_core7.setApiResponse)(
|
|
2144
|
+
import_core7.HTTP.FORBIDDEN,
|
|
2076
2145
|
"ACCOUNT_SUSPENDED",
|
|
2077
2146
|
"Account suspended. Please contact support."
|
|
2078
2147
|
);
|
|
@@ -2080,7 +2149,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2080
2149
|
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2081
2150
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
2082
2151
|
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
2083
|
-
await (0,
|
|
2152
|
+
await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
2084
2153
|
type: MESSAGE_KEYS.phoneOtp,
|
|
2085
2154
|
locale: user.locale,
|
|
2086
2155
|
data: { otp },
|
|
@@ -2096,8 +2165,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2096
2165
|
{ status: 202 }
|
|
2097
2166
|
);
|
|
2098
2167
|
}
|
|
2099
|
-
return (0,
|
|
2100
|
-
|
|
2168
|
+
return (0, import_core7.setApiResponse)(
|
|
2169
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
2101
2170
|
"INVALID_PARAMETER",
|
|
2102
2171
|
"Provide email + password or a valid phone number"
|
|
2103
2172
|
);
|
|
@@ -2122,35 +2191,35 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2122
2191
|
refresh: async (ctx) => {
|
|
2123
2192
|
const token = extractRefreshToken(ctx);
|
|
2124
2193
|
if (!token) {
|
|
2125
|
-
return (0,
|
|
2194
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
|
|
2126
2195
|
}
|
|
2127
2196
|
const payload = verifyToken(token, config);
|
|
2128
2197
|
if (!payload || payload.type !== "refresh") {
|
|
2129
|
-
return (0,
|
|
2198
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
2130
2199
|
}
|
|
2131
2200
|
const runtime = config.resolve?.(ctx);
|
|
2132
2201
|
let issued = null;
|
|
2133
2202
|
for (let attempt = 0; attempt < 3 && !issued; attempt++) {
|
|
2134
2203
|
const found = await sessions.match(token);
|
|
2135
2204
|
if (found.kind === "unknown") {
|
|
2136
|
-
return (0,
|
|
2205
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2137
2206
|
}
|
|
2138
2207
|
if (found.kind === "reused") {
|
|
2139
2208
|
await sessions.revokeById(found.row.id);
|
|
2140
|
-
await (0,
|
|
2209
|
+
await (0, import_core7.background)(
|
|
2141
2210
|
bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sids: [found.row.sid], reason: "refresh-reuse" })
|
|
2142
2211
|
);
|
|
2143
|
-
return (0,
|
|
2212
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2144
2213
|
}
|
|
2145
2214
|
const resolvedRefresh = configForClient(config, runtime, found.row.clientKind);
|
|
2146
2215
|
const maxAgeMs = resolvedRefresh.sessionMaxAge ? durationMs(String(resolvedRefresh.sessionMaxAge)) : null;
|
|
2147
2216
|
if (maxAgeMs !== null && Date.now() - found.row.createdAt.getTime() > maxAgeMs) {
|
|
2148
2217
|
await sessions.revokeById(found.row.id);
|
|
2149
|
-
return (0,
|
|
2218
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
|
|
2150
2219
|
}
|
|
2151
2220
|
const user = await users.findById(found.row.userId);
|
|
2152
2221
|
if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
|
|
2153
|
-
return (0,
|
|
2222
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
2154
2223
|
}
|
|
2155
2224
|
const pair = issueTokenPair(user.id, resolvedRefresh, {
|
|
2156
2225
|
loginMethod: payload.loginMethod ?? "email",
|
|
@@ -2163,7 +2232,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2163
2232
|
if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
|
|
2164
2233
|
}
|
|
2165
2234
|
if (!issued) {
|
|
2166
|
-
return (0,
|
|
2235
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2167
2236
|
}
|
|
2168
2237
|
const { accessToken, refreshToken } = issued;
|
|
2169
2238
|
return Response.json(
|
|
@@ -2182,16 +2251,16 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2182
2251
|
const body = ctx.meta["body"];
|
|
2183
2252
|
const rawEmail = body?.["email"];
|
|
2184
2253
|
if (typeof rawEmail !== "string") {
|
|
2185
|
-
return (0,
|
|
2254
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
2186
2255
|
}
|
|
2187
2256
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
2188
2257
|
if (!email2) {
|
|
2189
|
-
return (0,
|
|
2258
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
2190
2259
|
}
|
|
2191
2260
|
const user = await users.findByEmail(email2);
|
|
2192
2261
|
if (!user) {
|
|
2193
|
-
return (0,
|
|
2194
|
-
|
|
2262
|
+
return (0, import_core7.setApiResponse)(
|
|
2263
|
+
import_core7.HTTP.OK,
|
|
2195
2264
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
2196
2265
|
"Password reset email sent (if account exists)."
|
|
2197
2266
|
);
|
|
@@ -2200,8 +2269,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2200
2269
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
2201
2270
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
2202
2271
|
if (remaining > 0) {
|
|
2203
|
-
return (0,
|
|
2204
|
-
|
|
2272
|
+
return (0, import_core7.setApiResponse)(
|
|
2273
|
+
import_core7.HTTP.OK,
|
|
2205
2274
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
2206
2275
|
"Password reset email sent (if account exists)."
|
|
2207
2276
|
);
|
|
@@ -2212,14 +2281,14 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2212
2281
|
await passwordReset.create(user.id, pin, token, expiresAt);
|
|
2213
2282
|
const base = resolved.passwordResetUrl;
|
|
2214
2283
|
const resetUrl = base ? `${base}${base.includes("?") ? "&" : "?"}token=${token}` : "";
|
|
2215
|
-
await (0,
|
|
2284
|
+
await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
2216
2285
|
type: MESSAGE_KEYS.passwordReset,
|
|
2217
2286
|
locale: user.locale,
|
|
2218
2287
|
recipient: { email: email2, phone: null, deviceToken: null },
|
|
2219
2288
|
data: { pin, token, resetUrl }
|
|
2220
2289
|
}));
|
|
2221
|
-
return (0,
|
|
2222
|
-
|
|
2290
|
+
return (0, import_core7.setApiResponse)(
|
|
2291
|
+
import_core7.HTTP.OK,
|
|
2223
2292
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
2224
2293
|
"Password reset email sent (if account exists)."
|
|
2225
2294
|
);
|
|
@@ -2230,11 +2299,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2230
2299
|
const password2 = body?.["password"];
|
|
2231
2300
|
const rawToken = body?.["token"];
|
|
2232
2301
|
if (typeof password2 !== "string") {
|
|
2233
|
-
return (0,
|
|
2302
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
|
|
2234
2303
|
}
|
|
2235
2304
|
if (password2.length < 8) {
|
|
2236
|
-
return (0,
|
|
2237
|
-
|
|
2305
|
+
return (0, import_core7.setApiResponse)(
|
|
2306
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
2238
2307
|
"INVALID_PARAMETER",
|
|
2239
2308
|
"password must be at least 8 characters"
|
|
2240
2309
|
);
|
|
@@ -2244,8 +2313,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2244
2313
|
row = await passwordReset.findByToken(rawToken.trim());
|
|
2245
2314
|
} else {
|
|
2246
2315
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
2247
|
-
return (0,
|
|
2248
|
-
|
|
2316
|
+
return (0, import_core7.setApiResponse)(
|
|
2317
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
2249
2318
|
"INVALID_PARAMETER",
|
|
2250
2319
|
"a 6-digit pin or a reset token is required"
|
|
2251
2320
|
);
|
|
@@ -2253,33 +2322,41 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2253
2322
|
row = await passwordReset.findByPin(raw.trim());
|
|
2254
2323
|
}
|
|
2255
2324
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
2256
|
-
return (0,
|
|
2257
|
-
|
|
2325
|
+
return (0, import_core7.setApiResponse)(
|
|
2326
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2258
2327
|
"PASSWORD_RESET_FAILED",
|
|
2259
2328
|
"Invalid or expired reset credentials"
|
|
2260
2329
|
);
|
|
2261
2330
|
}
|
|
2262
2331
|
const passwordHash = await hashPassword(password2);
|
|
2263
|
-
await store.
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2332
|
+
const [changed] = await store.query(
|
|
2333
|
+
`WITH changed AS (
|
|
2334
|
+
UPDATE fonderie_users SET password_hash = $1, updated_at = now()
|
|
2335
|
+
WHERE id = $2 AND deleted_at IS NULL
|
|
2336
|
+
RETURNING id
|
|
2337
|
+
), spent AS (
|
|
2338
|
+
DELETE FROM fonderie_password_resets WHERE user_id = $2
|
|
2339
|
+
), revoked AS (
|
|
2340
|
+
DELETE FROM fonderie_sessions WHERE user_id IN (SELECT id FROM changed)
|
|
2341
|
+
)
|
|
2342
|
+
SELECT id FROM changed`,
|
|
2343
|
+
[passwordHash, row.userId]
|
|
2344
|
+
);
|
|
2345
|
+
if (!changed) {
|
|
2346
|
+
return (0, import_core7.setApiResponse)(
|
|
2347
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2348
|
+
"PASSWORD_RESET_FAILED",
|
|
2349
|
+
"Invalid or expired reset credentials"
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
|
|
2276
2353
|
},
|
|
2277
2354
|
verify: async (ctx) => {
|
|
2278
2355
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
2279
|
-
return (0,
|
|
2356
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
2280
2357
|
}
|
|
2281
2358
|
if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
|
|
2282
|
-
return (0,
|
|
2359
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
2283
2360
|
verified: true,
|
|
2284
2361
|
email: ctx.user.email
|
|
2285
2362
|
});
|
|
@@ -2287,8 +2364,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2287
2364
|
const body = ctx.meta["body"];
|
|
2288
2365
|
const raw = body?.["token"];
|
|
2289
2366
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
2290
|
-
return (0,
|
|
2291
|
-
|
|
2367
|
+
return (0, import_core7.setApiResponse)(
|
|
2368
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
2292
2369
|
"INVALID_PARAMETER",
|
|
2293
2370
|
"token must be a 6-digit code"
|
|
2294
2371
|
);
|
|
@@ -2306,7 +2383,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2306
2383
|
failureReason: "invalid_pin",
|
|
2307
2384
|
...phoneMeta
|
|
2308
2385
|
});
|
|
2309
|
-
return (0,
|
|
2386
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
2310
2387
|
}
|
|
2311
2388
|
if (/* @__PURE__ */ new Date() > record.expiresAt) {
|
|
2312
2389
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
@@ -2318,8 +2395,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2318
2395
|
failureReason: "expired_pin",
|
|
2319
2396
|
...phoneMeta
|
|
2320
2397
|
});
|
|
2321
|
-
return (0,
|
|
2322
|
-
|
|
2398
|
+
return (0, import_core7.setApiResponse)(
|
|
2399
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2323
2400
|
"VERIFICATION_FAILED",
|
|
2324
2401
|
"Verification code expired"
|
|
2325
2402
|
);
|
|
@@ -2339,11 +2416,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2339
2416
|
});
|
|
2340
2417
|
const verifiedUser = await users.findById(ctx.user.id);
|
|
2341
2418
|
if (!verifiedUser) {
|
|
2342
|
-
return (0,
|
|
2419
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2343
2420
|
}
|
|
2344
2421
|
if (verifiedUser.suspended) {
|
|
2345
|
-
return (0,
|
|
2346
|
-
|
|
2422
|
+
return (0, import_core7.setApiResponse)(
|
|
2423
|
+
import_core7.HTTP.FORBIDDEN,
|
|
2347
2424
|
"ACCOUNT_SUSPENDED",
|
|
2348
2425
|
"Account suspended. Please contact support."
|
|
2349
2426
|
);
|
|
@@ -2365,10 +2442,10 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2365
2442
|
}
|
|
2366
2443
|
const row = await emailVerif.findByUser(ctx.user.id, pin);
|
|
2367
2444
|
if (!row) {
|
|
2368
|
-
return (0,
|
|
2445
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
2369
2446
|
}
|
|
2370
2447
|
if (/* @__PURE__ */ new Date() > row.expiresAt) {
|
|
2371
|
-
return (0,
|
|
2448
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
|
|
2372
2449
|
}
|
|
2373
2450
|
await store.transaction(async (tx) => {
|
|
2374
2451
|
await Promise.all([
|
|
@@ -2382,30 +2459,30 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2382
2459
|
])
|
|
2383
2460
|
]);
|
|
2384
2461
|
});
|
|
2385
|
-
return (0,
|
|
2462
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
2386
2463
|
verified: true,
|
|
2387
2464
|
email: ctx.user.email
|
|
2388
2465
|
});
|
|
2389
2466
|
},
|
|
2390
2467
|
sendVerification: async (ctx) => {
|
|
2391
2468
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
2392
|
-
return (0,
|
|
2469
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
2393
2470
|
}
|
|
2394
2471
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
2395
2472
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
2396
2473
|
if (ctx.user.loginMethod === "phone") {
|
|
2397
2474
|
const phone2 = ctx.user.phone;
|
|
2398
2475
|
if (!phone2) {
|
|
2399
|
-
return (0,
|
|
2400
|
-
|
|
2476
|
+
return (0, import_core7.setApiResponse)(
|
|
2477
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2401
2478
|
"NO_PHONE_ON_ACCOUNT",
|
|
2402
2479
|
"No phone number associated with this account"
|
|
2403
2480
|
);
|
|
2404
2481
|
}
|
|
2405
2482
|
const remaining2 = checkCooldown(await phoneVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
2406
2483
|
if (remaining2 > 0) {
|
|
2407
|
-
return (0,
|
|
2408
|
-
|
|
2484
|
+
return (0, import_core7.setApiResponse)(
|
|
2485
|
+
import_core7.HTTP.TOO_MANY_REQUESTS,
|
|
2409
2486
|
"VERIFICATION_COOLDOWN",
|
|
2410
2487
|
"Please wait before requesting a new code.",
|
|
2411
2488
|
{
|
|
@@ -2416,35 +2493,35 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2416
2493
|
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2417
2494
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
2418
2495
|
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
2419
|
-
await (0,
|
|
2496
|
+
await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
2420
2497
|
type: MESSAGE_KEYS.phoneOtp,
|
|
2421
2498
|
locale: ctx.user.locale,
|
|
2422
2499
|
data: { otp },
|
|
2423
2500
|
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
2424
2501
|
}));
|
|
2425
|
-
return (0,
|
|
2426
|
-
|
|
2502
|
+
return (0, import_core7.setApiResponse)(
|
|
2503
|
+
import_core7.HTTP.OK,
|
|
2427
2504
|
"VERIFICATION_SENT",
|
|
2428
2505
|
"A verification code has been sent to your phone."
|
|
2429
2506
|
);
|
|
2430
2507
|
}
|
|
2431
2508
|
if (!ctx.user.email) {
|
|
2432
|
-
return (0,
|
|
2433
|
-
|
|
2509
|
+
return (0, import_core7.setApiResponse)(
|
|
2510
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2434
2511
|
"NO_EMAIL_ON_ACCOUNT",
|
|
2435
2512
|
"No email address associated with this account"
|
|
2436
2513
|
);
|
|
2437
2514
|
}
|
|
2438
2515
|
if (ctx.user.emailVerifiedAt) {
|
|
2439
|
-
return (0,
|
|
2516
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "EMAIL_VERIFIED", "Email already verified.", {
|
|
2440
2517
|
verified: true,
|
|
2441
2518
|
email: ctx.user.email
|
|
2442
2519
|
});
|
|
2443
2520
|
}
|
|
2444
2521
|
const remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
2445
2522
|
if (remaining > 0) {
|
|
2446
|
-
return (0,
|
|
2447
|
-
|
|
2523
|
+
return (0, import_core7.setApiResponse)(
|
|
2524
|
+
import_core7.HTTP.TOO_MANY_REQUESTS,
|
|
2448
2525
|
"VERIFICATION_COOLDOWN",
|
|
2449
2526
|
"Please wait before requesting a new code.",
|
|
2450
2527
|
{
|
|
@@ -2455,13 +2532,13 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2455
2532
|
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2456
2533
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2457
2534
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2458
|
-
await (0,
|
|
2535
|
+
await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
2459
2536
|
type: MESSAGE_KEYS.emailVerification,
|
|
2460
2537
|
locale: ctx.user.locale,
|
|
2461
2538
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null },
|
|
2462
2539
|
data: { pin }
|
|
2463
2540
|
}));
|
|
2464
|
-
return (0,
|
|
2541
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
|
|
2465
2542
|
email: ctx.user.email
|
|
2466
2543
|
});
|
|
2467
2544
|
}
|
|
@@ -2470,11 +2547,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2470
2547
|
|
|
2471
2548
|
// src/controllers/user.controller.ts
|
|
2472
2549
|
var import_node_crypto7 = require("crypto");
|
|
2473
|
-
var
|
|
2550
|
+
var import_core9 = require("@fonderie/core");
|
|
2474
2551
|
var import_events3 = require("@fonderie/events");
|
|
2475
2552
|
|
|
2476
2553
|
// src/dtos/login-activity.ts
|
|
2477
|
-
var
|
|
2554
|
+
var import_core8 = require("@fonderie/core");
|
|
2478
2555
|
function toLoginEventDTO(row) {
|
|
2479
2556
|
return {
|
|
2480
2557
|
id: row.id,
|
|
@@ -2491,10 +2568,10 @@ function toLoginEventDTO(row) {
|
|
|
2491
2568
|
}
|
|
2492
2569
|
function toLoginHistoryPageDTO(page) {
|
|
2493
2570
|
const last = page.events[page.events.length - 1];
|
|
2494
|
-
const nextCursor = page.hasMore && last ? (0,
|
|
2571
|
+
const nextCursor = page.hasMore && last ? (0, import_core8.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
|
|
2495
2572
|
return { events: page.events.map(toLoginEventDTO), nextCursor };
|
|
2496
2573
|
}
|
|
2497
|
-
var decodeLoginCursor =
|
|
2574
|
+
var decodeLoginCursor = import_core8.decodeKeysetCursor;
|
|
2498
2575
|
function toSessionDTO(row, currentSid) {
|
|
2499
2576
|
return {
|
|
2500
2577
|
id: row.id,
|
|
@@ -2525,9 +2602,9 @@ function userController(store, config, bus) {
|
|
|
2525
2602
|
me: async (ctx) => {
|
|
2526
2603
|
const user = await users.findById(ctx.user.id);
|
|
2527
2604
|
if (!user) {
|
|
2528
|
-
return (0,
|
|
2605
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2529
2606
|
}
|
|
2530
|
-
return (0,
|
|
2607
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
|
|
2531
2608
|
user: toUserDTO(user, ctx.user.phoneVerified)
|
|
2532
2609
|
});
|
|
2533
2610
|
},
|
|
@@ -2538,14 +2615,14 @@ function userController(store, config, bus) {
|
|
|
2538
2615
|
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
2539
2616
|
const outcomeParam = params.get("outcome");
|
|
2540
2617
|
if (outcomeParam !== null && outcomeParam !== "success" && outcomeParam !== "failed") {
|
|
2541
|
-
return (0,
|
|
2618
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
|
|
2542
2619
|
}
|
|
2543
2620
|
const cursorParam = params.get("cursor");
|
|
2544
2621
|
let cursor = null;
|
|
2545
2622
|
if (cursorParam) {
|
|
2546
2623
|
cursor = decodeLoginCursor(cursorParam);
|
|
2547
2624
|
if (!cursor) {
|
|
2548
|
-
return (0,
|
|
2625
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
2549
2626
|
}
|
|
2550
2627
|
}
|
|
2551
2628
|
const query = { userId: ctx.user.id, limit };
|
|
@@ -2556,8 +2633,8 @@ function userController(store, config, bus) {
|
|
|
2556
2633
|
if (from) query.from = new Date(from);
|
|
2557
2634
|
if (to) query.to = new Date(to);
|
|
2558
2635
|
const page = await loginEvents.listByUser(query);
|
|
2559
|
-
return (0,
|
|
2560
|
-
|
|
2636
|
+
return (0, import_core9.setApiResponse)(
|
|
2637
|
+
import_core9.HTTP.OK,
|
|
2561
2638
|
"LOGIN_HISTORY_FETCHED",
|
|
2562
2639
|
"Login history retrieved.",
|
|
2563
2640
|
toLoginHistoryPageDTO(page)
|
|
@@ -2567,35 +2644,35 @@ function userController(store, config, bus) {
|
|
|
2567
2644
|
listSessions: async (ctx) => {
|
|
2568
2645
|
const rows = await sessions.listLiveByUser(ctx.user.id);
|
|
2569
2646
|
const currentSid = ctx.user.sid ?? null;
|
|
2570
|
-
return (0,
|
|
2647
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
|
|
2571
2648
|
sessions: rows.map((r) => toSessionDTO(r, currentSid))
|
|
2572
2649
|
});
|
|
2573
2650
|
},
|
|
2574
2651
|
terminateSession: async (ctx) => {
|
|
2575
2652
|
const id = ctx.meta["params"]?.["id"];
|
|
2576
2653
|
if (!id) {
|
|
2577
|
-
return (0,
|
|
2654
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2578
2655
|
}
|
|
2579
2656
|
const removed = await sessions.terminateByIdReturningSid(ctx.user.id, id);
|
|
2580
2657
|
if (removed === void 0) {
|
|
2581
|
-
return (0,
|
|
2658
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2582
2659
|
}
|
|
2583
|
-
await (0,
|
|
2584
|
-
return (0,
|
|
2660
|
+
await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: [removed], reason: "terminated" }));
|
|
2661
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "SESSION_TERMINATED", "Session terminated.", { id });
|
|
2585
2662
|
},
|
|
2586
2663
|
terminateOtherSessions: async (ctx) => {
|
|
2587
2664
|
const currentSid = ctx.user.sid ?? null;
|
|
2588
2665
|
if (!currentSid) {
|
|
2589
|
-
return (0,
|
|
2590
|
-
|
|
2666
|
+
return (0, import_core9.setApiResponse)(
|
|
2667
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2591
2668
|
"NO_CURRENT_SESSION",
|
|
2592
2669
|
"Cannot terminate others without a current session context"
|
|
2593
2670
|
);
|
|
2594
2671
|
}
|
|
2595
2672
|
const sids = await sessions.terminateOthersReturningSids(ctx.user.id, currentSid);
|
|
2596
2673
|
const count = sids.length;
|
|
2597
|
-
if (count) await (0,
|
|
2598
|
-
return (0,
|
|
2674
|
+
if (count) await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids, reason: "terminated" }));
|
|
2675
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
|
|
2599
2676
|
},
|
|
2600
2677
|
updateProfile: async (ctx) => {
|
|
2601
2678
|
const body = ctx.meta["body"];
|
|
@@ -2605,18 +2682,18 @@ function userController(store, config, bus) {
|
|
|
2605
2682
|
if (body?.[key] !== void 0) fields[key] = body[key];
|
|
2606
2683
|
}
|
|
2607
2684
|
if (Object.keys(fields).length === 0) {
|
|
2608
|
-
return (0,
|
|
2609
|
-
|
|
2685
|
+
return (0, import_core9.setApiResponse)(
|
|
2686
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2610
2687
|
"INVALID_PARAMETER",
|
|
2611
2688
|
"Provide at least one of: firstName, lastName, avatarUrl"
|
|
2612
2689
|
);
|
|
2613
2690
|
}
|
|
2614
2691
|
const row = await users.update(ctx.user.id, fields);
|
|
2615
2692
|
if (!row) {
|
|
2616
|
-
return (0,
|
|
2693
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2617
2694
|
}
|
|
2618
2695
|
const updated = await users.findById(ctx.user.id);
|
|
2619
|
-
return (0,
|
|
2696
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "PROFILE_UPDATED", "Profile updated.", {
|
|
2620
2697
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2621
2698
|
});
|
|
2622
2699
|
},
|
|
@@ -2633,14 +2710,14 @@ function userController(store, config, bus) {
|
|
|
2633
2710
|
if (Object.keys(patch).length > 0) fields.patch = patch;
|
|
2634
2711
|
const row = await users.updatePreferences(ctx.user.id, fields);
|
|
2635
2712
|
if (!row) {
|
|
2636
|
-
return (0,
|
|
2637
|
-
|
|
2713
|
+
return (0, import_core9.setApiResponse)(
|
|
2714
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2638
2715
|
"INVALID_PARAMETER",
|
|
2639
2716
|
"Provide at least one preference field"
|
|
2640
2717
|
);
|
|
2641
2718
|
}
|
|
2642
2719
|
const updated = await users.findById(ctx.user.id);
|
|
2643
|
-
return (0,
|
|
2720
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
|
|
2644
2721
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2645
2722
|
});
|
|
2646
2723
|
},
|
|
@@ -2649,44 +2726,44 @@ function userController(store, config, bus) {
|
|
|
2649
2726
|
const newEmail = body?.["email"];
|
|
2650
2727
|
const normalised = typeof newEmail === "string" ? normalizeEmailSafe(newEmail) : null;
|
|
2651
2728
|
if (!normalised) {
|
|
2652
|
-
return (0,
|
|
2653
|
-
|
|
2729
|
+
return (0, import_core9.setApiResponse)(
|
|
2730
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2654
2731
|
"INVALID_PARAMETER",
|
|
2655
2732
|
"A valid email address is required"
|
|
2656
2733
|
);
|
|
2657
2734
|
}
|
|
2658
2735
|
const oldEmail = ctx.user.email;
|
|
2659
2736
|
if (normalised === oldEmail) {
|
|
2660
|
-
return (0,
|
|
2661
|
-
|
|
2737
|
+
return (0, import_core9.setApiResponse)(
|
|
2738
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2662
2739
|
"INVALID_PARAMETER",
|
|
2663
2740
|
"New email must differ from current email"
|
|
2664
2741
|
);
|
|
2665
2742
|
}
|
|
2666
2743
|
const existing = await users.findByEmail(normalised);
|
|
2667
2744
|
if (existing) {
|
|
2668
|
-
return (0,
|
|
2745
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
2669
2746
|
}
|
|
2670
2747
|
const pin = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
|
|
2671
2748
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2672
2749
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2673
2750
|
await users.updateEmail(ctx.user.id, normalised);
|
|
2674
|
-
await (0,
|
|
2751
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2675
2752
|
type: MESSAGE_KEYS.emailVerification,
|
|
2676
2753
|
locale: ctx.user.locale,
|
|
2677
2754
|
data: { pin },
|
|
2678
2755
|
recipient: { email: normalised, phone: null, deviceToken: null }
|
|
2679
2756
|
}));
|
|
2680
2757
|
if (oldEmail) {
|
|
2681
|
-
await (0,
|
|
2758
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2682
2759
|
type: MESSAGE_KEYS.emailChanged,
|
|
2683
2760
|
locale: ctx.user.locale,
|
|
2684
2761
|
data: { newEmail: normalised },
|
|
2685
2762
|
recipient: { email: oldEmail, phone: null, deviceToken: null }
|
|
2686
2763
|
}));
|
|
2687
2764
|
}
|
|
2688
|
-
return (0,
|
|
2689
|
-
|
|
2765
|
+
return (0, import_core9.setApiResponse)(
|
|
2766
|
+
import_core9.HTTP.OK,
|
|
2690
2767
|
"EMAIL_UPDATED",
|
|
2691
2768
|
"Email updated. A verification code has been sent to your new address.",
|
|
2692
2769
|
{
|
|
@@ -2698,8 +2775,8 @@ function userController(store, config, bus) {
|
|
|
2698
2775
|
const body = ctx.meta["body"];
|
|
2699
2776
|
const newPhone = body?.["phone"];
|
|
2700
2777
|
if (!isValidPhone2(newPhone)) {
|
|
2701
|
-
return (0,
|
|
2702
|
-
|
|
2778
|
+
return (0, import_core9.setApiResponse)(
|
|
2779
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2703
2780
|
"INVALID_PARAMETER",
|
|
2704
2781
|
"A valid phone number is required"
|
|
2705
2782
|
);
|
|
@@ -2707,28 +2784,28 @@ function userController(store, config, bus) {
|
|
|
2707
2784
|
const normalised = normalizePhone2(newPhone);
|
|
2708
2785
|
const existing = await users.findByPhone(normalised);
|
|
2709
2786
|
if (existing) {
|
|
2710
|
-
return (0,
|
|
2787
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
2711
2788
|
}
|
|
2712
2789
|
const otp = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
|
|
2713
2790
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
2714
2791
|
await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
|
|
2715
2792
|
await users.updatePhone(ctx.user.id, normalised);
|
|
2716
|
-
await (0,
|
|
2793
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2717
2794
|
type: MESSAGE_KEYS.phoneOtp,
|
|
2718
2795
|
locale: ctx.user.locale,
|
|
2719
2796
|
data: { otp },
|
|
2720
2797
|
recipient: { email: null, phone: normalised, deviceToken: null }
|
|
2721
2798
|
}));
|
|
2722
2799
|
if (ctx.user.email) {
|
|
2723
|
-
await (0,
|
|
2800
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2724
2801
|
type: MESSAGE_KEYS.phoneChanged,
|
|
2725
2802
|
locale: ctx.user.locale,
|
|
2726
2803
|
data: {},
|
|
2727
2804
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
2728
2805
|
}));
|
|
2729
2806
|
}
|
|
2730
|
-
return (0,
|
|
2731
|
-
|
|
2807
|
+
return (0, import_core9.setApiResponse)(
|
|
2808
|
+
import_core9.HTTP.OK,
|
|
2732
2809
|
"PHONE_UPDATED",
|
|
2733
2810
|
"Phone number updated. A verification code has been sent to your new number.",
|
|
2734
2811
|
{
|
|
@@ -2741,23 +2818,23 @@ function userController(store, config, bus) {
|
|
|
2741
2818
|
const currentPassword = body?.["currentPassword"];
|
|
2742
2819
|
const newPassword = body?.["newPassword"];
|
|
2743
2820
|
if (typeof currentPassword !== "string" || !currentPassword) {
|
|
2744
|
-
return (0,
|
|
2821
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
|
|
2745
2822
|
}
|
|
2746
2823
|
if (typeof newPassword !== "string" || newPassword.length < 8) {
|
|
2747
|
-
return (0,
|
|
2824
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
|
|
2748
2825
|
}
|
|
2749
2826
|
const user = await users.findById(ctx.user.id);
|
|
2750
|
-
if (!user) return (0,
|
|
2827
|
+
if (!user) return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2751
2828
|
const { hashPassword: hashPassword2, verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password(), password_exports));
|
|
2752
2829
|
const valid = await verifyPassword2(currentPassword, user.passwordHash ?? "");
|
|
2753
2830
|
if (!valid) {
|
|
2754
|
-
return (0,
|
|
2831
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
|
|
2755
2832
|
}
|
|
2756
2833
|
const hash = await hashPassword2(newPassword);
|
|
2757
2834
|
await users.updatePassword(ctx.user.id, hash);
|
|
2758
2835
|
await sessions.deleteByUser(ctx.user.id);
|
|
2759
|
-
await (0,
|
|
2760
|
-
return (0,
|
|
2836
|
+
await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: null, reason: "password-changed" }));
|
|
2837
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "PASSWORD_CHANGED", "Password updated successfully.");
|
|
2761
2838
|
},
|
|
2762
2839
|
/**
|
|
2763
2840
|
* Remove an OAuth provider from the account.
|
|
@@ -2776,44 +2853,50 @@ function userController(store, config, bus) {
|
|
|
2776
2853
|
const provider = String(ctx.meta["params"]?.["provider"] ?? "");
|
|
2777
2854
|
const current = await users.findById(ctx.user.id);
|
|
2778
2855
|
if (!current?.provider) {
|
|
2779
|
-
return (0,
|
|
2856
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_LINKED", "No sign-in provider is linked.");
|
|
2780
2857
|
}
|
|
2781
2858
|
if (provider && provider.toLowerCase() !== current.provider.toLowerCase()) {
|
|
2782
|
-
return (0,
|
|
2783
|
-
|
|
2859
|
+
return (0, import_core9.setApiResponse)(
|
|
2860
|
+
import_core9.HTTP.NOT_FOUND,
|
|
2784
2861
|
"NOT_LINKED",
|
|
2785
2862
|
`This account is not linked to ${provider}.`
|
|
2786
2863
|
);
|
|
2787
2864
|
}
|
|
2788
2865
|
const removed = await users.clearProvider(ctx.user.id);
|
|
2789
2866
|
if (!removed) {
|
|
2790
|
-
return (0,
|
|
2791
|
-
|
|
2867
|
+
return (0, import_core9.setApiResponse)(
|
|
2868
|
+
import_core9.HTTP.CONFLICT,
|
|
2792
2869
|
"PASSWORD_REQUIRED",
|
|
2793
2870
|
"Set a password before disconnecting this provider \u2014 it is currently the only way to sign in."
|
|
2794
2871
|
);
|
|
2795
2872
|
}
|
|
2796
2873
|
if (ctx.user.email) {
|
|
2797
|
-
await (0,
|
|
2874
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2798
2875
|
type: MESSAGE_KEYS.oauthUnlinked,
|
|
2799
2876
|
locale: ctx.user.locale,
|
|
2800
2877
|
data: { provider: current.provider },
|
|
2801
2878
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
2802
2879
|
}));
|
|
2803
2880
|
}
|
|
2804
|
-
return (0,
|
|
2881
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
|
|
2805
2882
|
},
|
|
2806
2883
|
deleteMe: async (ctx) => {
|
|
2807
2884
|
const userId = ctx.user.id;
|
|
2808
2885
|
await users.softDelete(userId);
|
|
2809
2886
|
await sessions.deleteByUser(userId);
|
|
2887
|
+
await store.query(
|
|
2888
|
+
`WITH resets AS (DELETE FROM fonderie_password_resets WHERE user_id = $1),
|
|
2889
|
+
emails AS (DELETE FROM fonderie_email_verifications WHERE user_id = $1)
|
|
2890
|
+
DELETE FROM fonderie_phone_verifications WHERE user_id = $1`,
|
|
2891
|
+
[userId]
|
|
2892
|
+
);
|
|
2810
2893
|
const reqId = ctx.meta["requestId"];
|
|
2811
|
-
await (0,
|
|
2894
|
+
await (0, import_core9.background)(bus?.emit(
|
|
2812
2895
|
EVENT_KEYS.userDeleted,
|
|
2813
2896
|
{ userId },
|
|
2814
2897
|
reqId !== void 0 ? { requestId: reqId } : void 0
|
|
2815
2898
|
));
|
|
2816
|
-
await (0,
|
|
2899
|
+
await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId, sids: null, reason: "account-deleted" }));
|
|
2817
2900
|
return Response.json(
|
|
2818
2901
|
{ reason: "ACCOUNT_DELETED", explanation: "Account successfully deleted." },
|
|
2819
2902
|
{
|
|
@@ -2827,7 +2910,7 @@ function userController(store, config, bus) {
|
|
|
2827
2910
|
// no MFA secret, no session tokens (session metadata only).
|
|
2828
2911
|
exportMe: async (ctx) => {
|
|
2829
2912
|
const user = await users.findById(ctx.user.id);
|
|
2830
|
-
if (!user) return (0,
|
|
2913
|
+
if (!user) return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2831
2914
|
const sessionRows = await sessions.listByUser(ctx.user.id);
|
|
2832
2915
|
const bundle = {
|
|
2833
2916
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2836,8 +2919,8 @@ function userController(store, config, bus) {
|
|
|
2836
2919
|
id: s.id,
|
|
2837
2920
|
userAgent: s.userAgent,
|
|
2838
2921
|
ipAddress: s.ipAddress,
|
|
2839
|
-
createdAt: (0,
|
|
2840
|
-
expiresAt: (0,
|
|
2922
|
+
createdAt: (0, import_core9.dateOrEmpty)(s.createdAt),
|
|
2923
|
+
expiresAt: (0, import_core9.dateOrEmpty)(s.expiresAt)
|
|
2841
2924
|
})),
|
|
2842
2925
|
security: {
|
|
2843
2926
|
mfaEnabled: user.mfaEnabled === true,
|
|
@@ -2856,7 +2939,7 @@ function userController(store, config, bus) {
|
|
|
2856
2939
|
}
|
|
2857
2940
|
bundle["modules"] = modules;
|
|
2858
2941
|
}
|
|
2859
|
-
return (0,
|
|
2942
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "DATA_EXPORT", "Your account data.", bundle);
|
|
2860
2943
|
}
|
|
2861
2944
|
};
|
|
2862
2945
|
}
|
|
@@ -2864,7 +2947,7 @@ function userController(store, config, bus) {
|
|
|
2864
2947
|
// src/controllers/oauth.controller.ts
|
|
2865
2948
|
var import_node_crypto9 = require("crypto");
|
|
2866
2949
|
var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
|
|
2867
|
-
var
|
|
2950
|
+
var import_core10 = require("@fonderie/core");
|
|
2868
2951
|
var import_events4 = require("@fonderie/events");
|
|
2869
2952
|
|
|
2870
2953
|
// src/services/jwks.ts
|
|
@@ -3015,7 +3098,7 @@ function oauthController(store, config, bus) {
|
|
|
3015
3098
|
const label = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
3016
3099
|
const recipient = { email: user.email, phone: null, deviceToken: null };
|
|
3017
3100
|
if (upserted.clearedUnverifiedPassword && user.email) {
|
|
3018
|
-
await (0,
|
|
3101
|
+
await (0, import_core10.background)(bus.emit(
|
|
3019
3102
|
import_events4.NOTIFICATION_EVENT,
|
|
3020
3103
|
{
|
|
3021
3104
|
type: MESSAGE_KEYS.passwordRevoked,
|
|
@@ -3027,7 +3110,7 @@ function oauthController(store, config, bus) {
|
|
|
3027
3110
|
));
|
|
3028
3111
|
}
|
|
3029
3112
|
if (upserted.inserted) {
|
|
3030
|
-
await (0,
|
|
3113
|
+
await (0, import_core10.background)(bus.emit(
|
|
3031
3114
|
EVENT_KEYS.userRegistered,
|
|
3032
3115
|
{
|
|
3033
3116
|
userId: upserted.id,
|
|
@@ -3039,7 +3122,7 @@ function oauthController(store, config, bus) {
|
|
|
3039
3122
|
reqOpts
|
|
3040
3123
|
));
|
|
3041
3124
|
if (user.email) {
|
|
3042
|
-
await (0,
|
|
3125
|
+
await (0, import_core10.background)(bus.emit(
|
|
3043
3126
|
import_events4.NOTIFICATION_EVENT,
|
|
3044
3127
|
{
|
|
3045
3128
|
type: MESSAGE_KEYS.oauthRegistration,
|
|
@@ -3053,7 +3136,7 @@ function oauthController(store, config, bus) {
|
|
|
3053
3136
|
return;
|
|
3054
3137
|
}
|
|
3055
3138
|
if (upserted.previousProvider !== provider && user.email) {
|
|
3056
|
-
await (0,
|
|
3139
|
+
await (0, import_core10.background)(bus.emit(
|
|
3057
3140
|
import_events4.NOTIFICATION_EVENT,
|
|
3058
3141
|
{
|
|
3059
3142
|
type: MESSAGE_KEYS.oauthLinked,
|
|
@@ -3074,14 +3157,29 @@ function oauthController(store, config, bus) {
|
|
|
3074
3157
|
const method = provider === "apple" ? "oauth-apple" : "oauth-google";
|
|
3075
3158
|
const meta = requestMeta(ctx);
|
|
3076
3159
|
if (!claims.email) {
|
|
3077
|
-
return (0,
|
|
3160
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, p.failed, `No email in ${p.label} identity token`);
|
|
3078
3161
|
}
|
|
3079
3162
|
if (!claims.emailVerified) {
|
|
3080
|
-
return (0,
|
|
3163
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, p.failed, `${p.label} account email is not verified`);
|
|
3081
3164
|
}
|
|
3082
3165
|
const normalizedEmail = normalizeEmailSafe(claims.email);
|
|
3083
3166
|
if (!normalizedEmail) {
|
|
3084
|
-
return (0,
|
|
3167
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, p.failed, `Invalid email in ${p.label} identity token`);
|
|
3168
|
+
}
|
|
3169
|
+
const archived = await users.findArchivedByEmail(normalizedEmail);
|
|
3170
|
+
if (archived?.deletedAt) {
|
|
3171
|
+
await loginEvents.recordSafe({
|
|
3172
|
+
userId: archived.id,
|
|
3173
|
+
emailAttempted: normalizedEmail,
|
|
3174
|
+
method,
|
|
3175
|
+
outcome: "failed",
|
|
3176
|
+
failureReason: "pending_deletion",
|
|
3177
|
+
...meta
|
|
3178
|
+
});
|
|
3179
|
+
const res = pendingDeletionResponse(archived.deletedAt, config);
|
|
3180
|
+
const headers = cookieHeaders(clearCookies);
|
|
3181
|
+
headers.set("content-type", res.headers.get("content-type") ?? "application/json");
|
|
3182
|
+
return new Response(res.body, { status: res.status, headers });
|
|
3085
3183
|
}
|
|
3086
3184
|
const upserted = await users.upsertByProvider(normalizedEmail, provider, claims.sub ?? "");
|
|
3087
3185
|
if (!upserted) {
|
|
@@ -3093,15 +3191,15 @@ function oauthController(store, config, bus) {
|
|
|
3093
3191
|
failureReason: "provider_upsert_failed",
|
|
3094
3192
|
...meta
|
|
3095
3193
|
});
|
|
3096
|
-
return (0,
|
|
3194
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
|
|
3097
3195
|
}
|
|
3098
3196
|
const fullUser = await users.findById(upserted.id);
|
|
3099
3197
|
if (!fullUser) {
|
|
3100
|
-
return (0,
|
|
3198
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
|
|
3101
3199
|
}
|
|
3102
3200
|
await announceOAuthUpsert(ctx, upserted, provider, fullUser);
|
|
3103
3201
|
if (fullUser.suspended) {
|
|
3104
|
-
return (0,
|
|
3202
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.FORBIDDEN, "ACCOUNT_SUSPENDED", "Account suspended. Please contact support.");
|
|
3105
3203
|
}
|
|
3106
3204
|
if (fullUser.mfaEnabled) {
|
|
3107
3205
|
const mfaToken = issueMfaPendingToken(upserted.id, config, provider);
|
|
@@ -3144,8 +3242,8 @@ function oauthController(store, config, bus) {
|
|
|
3144
3242
|
googleInit: async (_ctx) => {
|
|
3145
3243
|
const google = config.google;
|
|
3146
3244
|
if (!google || !google.clientSecret || !google.redirectUri) {
|
|
3147
|
-
return (0,
|
|
3148
|
-
|
|
3245
|
+
return (0, import_core10.setApiResponse)(
|
|
3246
|
+
import_core10.HTTP.NOT_IMPLEMENTED,
|
|
3149
3247
|
"NOT_CONFIGURED",
|
|
3150
3248
|
"Google OAuth not configured"
|
|
3151
3249
|
);
|
|
@@ -3179,8 +3277,8 @@ function oauthController(store, config, bus) {
|
|
|
3179
3277
|
googleCallback: async (ctx) => {
|
|
3180
3278
|
const google = config.google;
|
|
3181
3279
|
if (!google || !google.clientSecret || !google.redirectUri) {
|
|
3182
|
-
return (0,
|
|
3183
|
-
|
|
3280
|
+
return (0, import_core10.setApiResponse)(
|
|
3281
|
+
import_core10.HTTP.NOT_IMPLEMENTED,
|
|
3184
3282
|
"NOT_CONFIGURED",
|
|
3185
3283
|
"Google OAuth not configured"
|
|
3186
3284
|
);
|
|
@@ -3188,14 +3286,14 @@ function oauthController(store, config, bus) {
|
|
|
3188
3286
|
const url = new URL(ctx.request.url);
|
|
3189
3287
|
const code = url.searchParams.get("code");
|
|
3190
3288
|
if (!code) {
|
|
3191
|
-
return (0,
|
|
3289
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
3192
3290
|
}
|
|
3193
3291
|
const returnedState = url.searchParams.get("state") ?? "";
|
|
3194
3292
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
3195
3293
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
3196
|
-
if (!returnedState || !expectedState || !(0,
|
|
3197
|
-
return (0,
|
|
3198
|
-
|
|
3294
|
+
if (!returnedState || !expectedState || !(0, import_core10.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
|
|
3295
|
+
return (0, import_core10.setApiResponse)(
|
|
3296
|
+
import_core10.HTTP.BAD_REQUEST,
|
|
3199
3297
|
"GOOGLE_AUTH_FAILED",
|
|
3200
3298
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
3201
3299
|
);
|
|
@@ -3213,8 +3311,8 @@ function oauthController(store, config, bus) {
|
|
|
3213
3311
|
});
|
|
3214
3312
|
const tokenData = await tokenRes.json();
|
|
3215
3313
|
if (!tokenData.id_token) {
|
|
3216
|
-
return (0,
|
|
3217
|
-
|
|
3314
|
+
return (0, import_core10.setApiResponse)(
|
|
3315
|
+
import_core10.HTTP.BAD_REQUEST,
|
|
3218
3316
|
"GOOGLE_AUTH_FAILED",
|
|
3219
3317
|
"OAuth token exchange failed"
|
|
3220
3318
|
);
|
|
@@ -3223,20 +3321,20 @@ function oauthController(store, config, bus) {
|
|
|
3223
3321
|
Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
|
|
3224
3322
|
);
|
|
3225
3323
|
if (payload.aud !== google.clientId) {
|
|
3226
|
-
return (0,
|
|
3324
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
|
|
3227
3325
|
}
|
|
3228
3326
|
if (payload.iss !== "https://accounts.google.com" && payload.iss !== "accounts.google.com") {
|
|
3229
|
-
return (0,
|
|
3327
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
|
|
3230
3328
|
}
|
|
3231
3329
|
if (typeof payload.exp !== "number" || payload.exp * 1e3 < Date.now()) {
|
|
3232
|
-
return (0,
|
|
3330
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
|
|
3233
3331
|
}
|
|
3234
3332
|
if (!payload.email) {
|
|
3235
|
-
return (0,
|
|
3333
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
|
|
3236
3334
|
}
|
|
3237
3335
|
if (payload.email_verified !== true) {
|
|
3238
|
-
return (0,
|
|
3239
|
-
|
|
3336
|
+
return (0, import_core10.setApiResponse)(
|
|
3337
|
+
import_core10.HTTP.BAD_REQUEST,
|
|
3240
3338
|
"GOOGLE_AUTH_FAILED",
|
|
3241
3339
|
"Google account email is not verified"
|
|
3242
3340
|
);
|
|
@@ -3250,7 +3348,7 @@ function oauthController(store, config, bus) {
|
|
|
3250
3348
|
appleInit: async (_ctx) => {
|
|
3251
3349
|
const apple = config.apple;
|
|
3252
3350
|
if (!apple) {
|
|
3253
|
-
return (0,
|
|
3351
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3254
3352
|
}
|
|
3255
3353
|
const state = (0, import_node_crypto9.randomBytes)(16).toString("hex");
|
|
3256
3354
|
const params = new URLSearchParams({
|
|
@@ -3281,20 +3379,20 @@ function oauthController(store, config, bus) {
|
|
|
3281
3379
|
appleCallback: async (ctx) => {
|
|
3282
3380
|
const apple = config.apple;
|
|
3283
3381
|
if (!apple) {
|
|
3284
|
-
return (0,
|
|
3382
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3285
3383
|
}
|
|
3286
3384
|
const pre = ctx.meta["body"];
|
|
3287
3385
|
const form = pre && typeof pre === "object" ? new URLSearchParams(pre) : new URLSearchParams(await ctx.request.text());
|
|
3288
3386
|
const code = form.get("code");
|
|
3289
3387
|
if (!code) {
|
|
3290
|
-
return (0,
|
|
3388
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
3291
3389
|
}
|
|
3292
3390
|
const returnedState = form.get("state") ?? "";
|
|
3293
3391
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
3294
3392
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
3295
|
-
if (!returnedState || !expectedState || !(0,
|
|
3296
|
-
return (0,
|
|
3297
|
-
|
|
3393
|
+
if (!returnedState || !expectedState || !(0, import_core10.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
|
|
3394
|
+
return (0, import_core10.setApiResponse)(
|
|
3395
|
+
import_core10.HTTP.BAD_REQUEST,
|
|
3298
3396
|
"APPLE_AUTH_FAILED",
|
|
3299
3397
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
3300
3398
|
);
|
|
@@ -3312,11 +3410,11 @@ function oauthController(store, config, bus) {
|
|
|
3312
3410
|
});
|
|
3313
3411
|
const tokenData = await tokenRes.json();
|
|
3314
3412
|
if (!tokenData.id_token) {
|
|
3315
|
-
return (0,
|
|
3413
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
|
|
3316
3414
|
}
|
|
3317
3415
|
const claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });
|
|
3318
3416
|
if (!claims) {
|
|
3319
|
-
return (0,
|
|
3417
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3320
3418
|
}
|
|
3321
3419
|
return completeOAuthLogin(ctx, "apple", {
|
|
3322
3420
|
email: claims.email,
|
|
@@ -3327,12 +3425,12 @@ function oauthController(store, config, bus) {
|
|
|
3327
3425
|
appleNative: async (ctx) => {
|
|
3328
3426
|
const apple = config.apple;
|
|
3329
3427
|
if (!apple) {
|
|
3330
|
-
return (0,
|
|
3428
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3331
3429
|
}
|
|
3332
3430
|
const body = ctx.meta["body"];
|
|
3333
3431
|
const identityToken = body?.identityToken;
|
|
3334
3432
|
if (!identityToken) {
|
|
3335
|
-
return (0,
|
|
3433
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
|
|
3336
3434
|
}
|
|
3337
3435
|
const audiences = apple.nativeClientIds && apple.nativeClientIds.length ? apple.nativeClientIds : [apple.clientId];
|
|
3338
3436
|
const claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });
|
|
@@ -3346,7 +3444,7 @@ function oauthController(store, config, bus) {
|
|
|
3346
3444
|
failureReason: "invalid_identity_token",
|
|
3347
3445
|
...meta
|
|
3348
3446
|
});
|
|
3349
|
-
return (0,
|
|
3447
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3350
3448
|
}
|
|
3351
3449
|
const tokenHash = (0, import_node_crypto9.createHash)("sha256").update(identityToken).digest("hex");
|
|
3352
3450
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
@@ -3355,14 +3453,14 @@ function oauthController(store, config, bus) {
|
|
|
3355
3453
|
const meta = requestMeta(ctx);
|
|
3356
3454
|
await loginEvents.recordSafe({
|
|
3357
3455
|
userId: null,
|
|
3358
|
-
emailAttempted: claims.email ?? null,
|
|
3456
|
+
emailAttempted: claims.email ? normalizeEmailSafe(claims.email) ?? claims.email : null,
|
|
3359
3457
|
method: "oauth-apple",
|
|
3360
3458
|
outcome: "failed",
|
|
3361
3459
|
failureReason: "token_replayed",
|
|
3362
3460
|
...meta
|
|
3363
3461
|
});
|
|
3364
|
-
return (0,
|
|
3365
|
-
|
|
3462
|
+
return (0, import_core10.setApiResponse)(
|
|
3463
|
+
import_core10.HTTP.UNAUTHORIZED,
|
|
3366
3464
|
"APPLE_AUTH_FAILED",
|
|
3367
3465
|
"This Apple identity token has already been used"
|
|
3368
3466
|
);
|
|
@@ -3379,12 +3477,12 @@ function oauthController(store, config, bus) {
|
|
|
3379
3477
|
googleNative: async (ctx) => {
|
|
3380
3478
|
const audiences = config.google?.nativeClientIds ?? [];
|
|
3381
3479
|
if (!config.google || audiences.length === 0) {
|
|
3382
|
-
return (0,
|
|
3480
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Native Google sign-in not configured");
|
|
3383
3481
|
}
|
|
3384
3482
|
const body = ctx.meta["body"];
|
|
3385
3483
|
const idToken = body?.idToken;
|
|
3386
3484
|
if (!idToken) {
|
|
3387
|
-
return (0,
|
|
3485
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing idToken");
|
|
3388
3486
|
}
|
|
3389
3487
|
const claims = await verifyGoogleIdToken(idToken, { audiences, nonce: body?.nonce });
|
|
3390
3488
|
if (!claims) {
|
|
@@ -3396,20 +3494,20 @@ function oauthController(store, config, bus) {
|
|
|
3396
3494
|
failureReason: "invalid_identity_token",
|
|
3397
3495
|
...requestMeta(ctx)
|
|
3398
3496
|
});
|
|
3399
|
-
return (0,
|
|
3497
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "Invalid Google ID token");
|
|
3400
3498
|
}
|
|
3401
3499
|
const tokenHash = (0, import_node_crypto9.createHash)("sha256").update(idToken).digest("hex");
|
|
3402
3500
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
3403
3501
|
if (!await consumedTokens.consumeOnce(tokenHash, new Date(expMs))) {
|
|
3404
3502
|
await loginEvents.recordSafe({
|
|
3405
3503
|
userId: null,
|
|
3406
|
-
emailAttempted: claims.email ?? null,
|
|
3504
|
+
emailAttempted: claims.email ? normalizeEmailSafe(claims.email) ?? claims.email : null,
|
|
3407
3505
|
method: "oauth-google",
|
|
3408
3506
|
outcome: "failed",
|
|
3409
3507
|
failureReason: "token_replayed",
|
|
3410
3508
|
...requestMeta(ctx)
|
|
3411
3509
|
});
|
|
3412
|
-
return (0,
|
|
3510
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "This Google ID token has already been used");
|
|
3413
3511
|
}
|
|
3414
3512
|
consumedTokens.sweepExpiredSafe();
|
|
3415
3513
|
return completeOAuthLogin(ctx, "google", {
|
|
@@ -3455,7 +3553,7 @@ function buildAuthRoutes(store, config, bus, locales) {
|
|
|
3455
3553
|
"providers",
|
|
3456
3554
|
"GET",
|
|
3457
3555
|
"/auth/providers",
|
|
3458
|
-
async () => (0,
|
|
3556
|
+
async () => (0, import_core11.setApiResponse)(import_core11.HTTP.OK, "AUTH_PROVIDERS", "Sign-in methods available here", {
|
|
3459
3557
|
providers: [...config.providers]
|
|
3460
3558
|
})
|
|
3461
3559
|
),
|
|
@@ -3530,20 +3628,20 @@ function buildAuthRoutes(store, config, bus, locales) {
|
|
|
3530
3628
|
}
|
|
3531
3629
|
|
|
3532
3630
|
// src/services/config-guard.ts
|
|
3533
|
-
var
|
|
3631
|
+
var import_core12 = require("@fonderie/core");
|
|
3534
3632
|
var MODULE = "@fonderie/auth";
|
|
3535
3633
|
function collectAuthConfigProblems(config) {
|
|
3536
3634
|
const problems = [];
|
|
3537
3635
|
const secret = config.jwtSecret ?? "";
|
|
3538
|
-
const secretProblem = (0,
|
|
3636
|
+
const secretProblem = (0, import_core12.secretStrengthProblem)(secret);
|
|
3539
3637
|
if (secretProblem === "too-short") {
|
|
3540
3638
|
problems.push({
|
|
3541
3639
|
module: MODULE,
|
|
3542
3640
|
severity: "error",
|
|
3543
|
-
message: `jwtSecret must be at least ${
|
|
3641
|
+
message: `jwtSecret must be at least ${import_core12.MIN_SECRET_LENGTH} characters (got ${secret.length})`,
|
|
3544
3642
|
domain: "auth",
|
|
3545
3643
|
reason: "JWT_SECRET_TOO_SHORT",
|
|
3546
|
-
metadata: { min:
|
|
3644
|
+
metadata: { min: import_core12.MIN_SECRET_LENGTH, got: secret.length }
|
|
3547
3645
|
});
|
|
3548
3646
|
} else if (secretProblem === "placeholder") {
|
|
3549
3647
|
problems.push({
|
|
@@ -3555,7 +3653,7 @@ function collectAuthConfigProblems(config) {
|
|
|
3555
3653
|
});
|
|
3556
3654
|
}
|
|
3557
3655
|
(config.jwtPreviousSecrets ?? []).forEach((previous, index) => {
|
|
3558
|
-
if ((0,
|
|
3656
|
+
if ((0, import_core12.secretStrengthProblem)(previous ?? "")) {
|
|
3559
3657
|
problems.push({
|
|
3560
3658
|
module: MODULE,
|
|
3561
3659
|
severity: "error",
|
|
@@ -3588,7 +3686,7 @@ function collectAuthConfigProblems(config) {
|
|
|
3588
3686
|
domain: "auth",
|
|
3589
3687
|
reason: "GOOGLE_INCOMPLETE"
|
|
3590
3688
|
});
|
|
3591
|
-
} else if (clientSecret &&
|
|
3689
|
+
} else if (clientSecret && import_core12.PLACEHOLDER_SECRET.test(clientSecret)) {
|
|
3592
3690
|
problems.push({
|
|
3593
3691
|
module: MODULE,
|
|
3594
3692
|
severity: "error",
|
|
@@ -3719,8 +3817,8 @@ function extractToken(request) {
|
|
|
3719
3817
|
}
|
|
3720
3818
|
|
|
3721
3819
|
// src/admin.ts
|
|
3722
|
-
var import_core12 = require("@fonderie/core");
|
|
3723
3820
|
var import_core13 = require("@fonderie/core");
|
|
3821
|
+
var import_core14 = require("@fonderie/core");
|
|
3724
3822
|
function toAdminUserDTO(user) {
|
|
3725
3823
|
return {
|
|
3726
3824
|
...toUserDTO(user),
|
|
@@ -3731,12 +3829,12 @@ function toAdminUserDTO(user) {
|
|
|
3731
3829
|
}
|
|
3732
3830
|
function toAdminUserPageDTO(page) {
|
|
3733
3831
|
const last = page.users[page.users.length - 1];
|
|
3734
|
-
const nextCursor = page.hasMore && last ? (0,
|
|
3832
|
+
const nextCursor = page.hasMore && last ? (0, import_core13.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
|
|
3735
3833
|
return { users: page.users.map(toAdminUserDTO), nextCursor };
|
|
3736
3834
|
}
|
|
3737
3835
|
var ADMIN_PREFIX = "/_admin";
|
|
3738
3836
|
var idOf = (ctx) => ctx.meta.params?.["id"] ?? "";
|
|
3739
|
-
var NOT_FOUND = () => (0,
|
|
3837
|
+
var NOT_FOUND = () => (0, import_core13.setApiResponse)(import_core13.HTTP.NOT_FOUND, "NOT_FOUND", "No such user");
|
|
3740
3838
|
function adminRouteTable(store, bus) {
|
|
3741
3839
|
const users = new UserModel(store);
|
|
3742
3840
|
const sessions = new SessionModel(store);
|
|
@@ -3745,8 +3843,8 @@ function adminRouteTable(store, bus) {
|
|
|
3745
3843
|
const ok = await users.setSuspended(idOf(ctx), suspended);
|
|
3746
3844
|
if (!ok) return NOT_FOUND();
|
|
3747
3845
|
const user = await users.findById(idOf(ctx));
|
|
3748
|
-
return user ? (0,
|
|
3749
|
-
|
|
3846
|
+
return user ? (0, import_core13.setApiResponse)(
|
|
3847
|
+
import_core13.HTTP.OK,
|
|
3750
3848
|
suspended ? "USER_SUSPENDED" : "USER_UNSUSPENDED",
|
|
3751
3849
|
suspended ? "User suspended" : "User unsuspended",
|
|
3752
3850
|
toAdminUserDTO(user)
|
|
@@ -3761,24 +3859,25 @@ function adminRouteTable(store, bus) {
|
|
|
3761
3859
|
// see anything cannot discover who signed up this morning.
|
|
3762
3860
|
async (ctx) => {
|
|
3763
3861
|
const params = new URL(ctx.request.url).searchParams;
|
|
3764
|
-
const
|
|
3862
|
+
const raw = params.get("email")?.trim();
|
|
3863
|
+
const email2 = raw ? normalizeEmailSafe(raw) ?? raw.toLowerCase() : void 0;
|
|
3765
3864
|
if (email2) {
|
|
3766
3865
|
const user = await users.findByEmail(email2);
|
|
3767
|
-
return user ? (0,
|
|
3866
|
+
return user ? (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
|
|
3768
3867
|
}
|
|
3769
3868
|
const rawLimit = Number(params.get("limit") ?? 50);
|
|
3770
3869
|
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
3771
3870
|
const cursorParam = params.get("cursor");
|
|
3772
3871
|
const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
|
|
3773
3872
|
if (cursorParam && !cursor)
|
|
3774
|
-
return (0,
|
|
3873
|
+
return (0, import_core13.setApiResponse)(import_core13.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
3775
3874
|
const deleted = params.get("deleted") === "1" || params.get("deleted") === "true";
|
|
3776
3875
|
const page = await users.list({
|
|
3777
3876
|
limit,
|
|
3778
3877
|
...cursor ? { cursor } : {},
|
|
3779
3878
|
...deleted ? { deleted } : {}
|
|
3780
3879
|
});
|
|
3781
|
-
return (0,
|
|
3880
|
+
return (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "USERS", "Users", toAdminUserPageDTO(page));
|
|
3782
3881
|
}
|
|
3783
3882
|
],
|
|
3784
3883
|
[
|
|
@@ -3786,7 +3885,7 @@ function adminRouteTable(store, bus) {
|
|
|
3786
3885
|
"/_admin/users/:id",
|
|
3787
3886
|
async (ctx) => {
|
|
3788
3887
|
const user = await users.findByIdIncludingDeleted(idOf(ctx));
|
|
3789
|
-
return user ? (0,
|
|
3888
|
+
return user ? (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
|
|
3790
3889
|
}
|
|
3791
3890
|
],
|
|
3792
3891
|
[
|
|
@@ -3795,8 +3894,8 @@ function adminRouteTable(store, bus) {
|
|
|
3795
3894
|
async (ctx) => {
|
|
3796
3895
|
if (!await users.findById(idOf(ctx))) return NOT_FOUND();
|
|
3797
3896
|
const rows = await sessions.listLiveByUser(idOf(ctx));
|
|
3798
|
-
return (0,
|
|
3799
|
-
|
|
3897
|
+
return (0, import_core13.setApiResponse)(
|
|
3898
|
+
import_core13.HTTP.OK,
|
|
3800
3899
|
"SESSIONS",
|
|
3801
3900
|
"Live sessions",
|
|
3802
3901
|
rows.map((r) => toSessionDTO(r, null))
|
|
@@ -3810,8 +3909,8 @@ function adminRouteTable(store, bus) {
|
|
|
3810
3909
|
async (ctx) => {
|
|
3811
3910
|
if (!await users.findById(idOf(ctx))) return NOT_FOUND();
|
|
3812
3911
|
await sessions.deleteByUser(idOf(ctx));
|
|
3813
|
-
await (0,
|
|
3814
|
-
return (0,
|
|
3912
|
+
await (0, import_core14.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: idOf(ctx), sids: null, reason: "admin" }));
|
|
3913
|
+
return (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "SESSIONS_REVOKED", "All sessions revoked");
|
|
3815
3914
|
}
|
|
3816
3915
|
],
|
|
3817
3916
|
[
|
|
@@ -3825,14 +3924,14 @@ function adminRouteTable(store, bus) {
|
|
|
3825
3924
|
const cursorParam = params.get("cursor");
|
|
3826
3925
|
const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
|
|
3827
3926
|
if (cursorParam && !cursor)
|
|
3828
|
-
return (0,
|
|
3927
|
+
return (0, import_core13.setApiResponse)(import_core13.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
3829
3928
|
const page = await events.listByUser({
|
|
3830
3929
|
userId: idOf(ctx),
|
|
3831
3930
|
limit,
|
|
3832
3931
|
...cursor ? { cursor } : {}
|
|
3833
3932
|
});
|
|
3834
|
-
return (0,
|
|
3835
|
-
|
|
3933
|
+
return (0, import_core13.setApiResponse)(
|
|
3934
|
+
import_core13.HTTP.OK,
|
|
3836
3935
|
"LOGIN_HISTORY",
|
|
3837
3936
|
"Login history",
|
|
3838
3937
|
toLoginHistoryPageDTO(page)
|
|
@@ -3865,7 +3964,7 @@ var AuthModule = class {
|
|
|
3865
3964
|
name = "@fonderie/auth";
|
|
3866
3965
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3867
3966
|
// say what is actually deployed rather than 'not reported'.
|
|
3868
|
-
version = "7.
|
|
3967
|
+
version = "7.23.0";
|
|
3869
3968
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3870
3969
|
describeAdmin() {
|
|
3871
3970
|
return { routes: describeAuthAdminRoutes(this.store, this.bus) };
|
|
@@ -3896,7 +3995,7 @@ var AuthModule = class {
|
|
|
3896
3995
|
};
|
|
3897
3996
|
|
|
3898
3997
|
// src/templates.ts
|
|
3899
|
-
var
|
|
3998
|
+
var import_core15 = require("@fonderie/core");
|
|
3900
3999
|
|
|
3901
4000
|
// src/templates.es.ts
|
|
3902
4001
|
var ES_TEMPLATES = {
|
|
@@ -4681,7 +4780,7 @@ change your password \u2014 someone else may be able to sign in as you.`
|
|
|
4681
4780
|
If you did this, you're all set. If not, contact support right away.`
|
|
4682
4781
|
}
|
|
4683
4782
|
};
|
|
4684
|
-
var DEFAULT_TEMPLATES = (0,
|
|
4783
|
+
var DEFAULT_TEMPLATES = (0, import_core15.withTranslations)(EN_TEMPLATES, {
|
|
4685
4784
|
fr: FR_TEMPLATES,
|
|
4686
4785
|
es: ES_TEMPLATES,
|
|
4687
4786
|
"zh-Hans": ZH_HANS_TEMPLATES,
|
|
@@ -4721,7 +4820,9 @@ async function importUser(store, user) {
|
|
|
4721
4820
|
placeholders.push(`$${vals.length}`);
|
|
4722
4821
|
};
|
|
4723
4822
|
if (user.id !== void 0) add("id", user.id);
|
|
4724
|
-
|
|
4823
|
+
const email2 = normalizeEmailSafe(user.email);
|
|
4824
|
+
if (!email2) throw new Error(`[auth] importUser: '${user.email}' is not an email address`);
|
|
4825
|
+
add("email", email2);
|
|
4725
4826
|
if (user.passwordHash !== void 0) add("password_hash", user.passwordHash);
|
|
4726
4827
|
if (user.firstName !== void 0) add("first_name", user.firstName);
|
|
4727
4828
|
if (user.lastName !== void 0) add("last_name", user.lastName);
|