@fonderie/auth 7.22.1 → 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/signatures.md +3 -0
- package/dist/index.cjs +320 -226
- 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 +278 -184
- 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 +1 -1
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ var init_password = __esm({
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
// src/routes.ts
|
|
47
|
-
import { setApiResponse as
|
|
47
|
+
import { setApiResponse as setApiResponse7, HTTP as HTTP7 } from "@fonderie/core";
|
|
48
48
|
import { requireAuth, requireAnyAuth, requireVerified } from "@fonderie/core/middlewares";
|
|
49
49
|
|
|
50
50
|
// src/middlewares/require-email-login.ts
|
|
@@ -338,6 +338,10 @@ var EVENT_KEYS = {
|
|
|
338
338
|
// 'password-changed', 'admin', 'refresh-reuse' (theft signal).
|
|
339
339
|
sessionRevoked: "fonderie.session.revoked"
|
|
340
340
|
};
|
|
341
|
+
var deletionGracePeriodDays = (config) => {
|
|
342
|
+
const days = config.accountDeletion?.gracePeriodDays;
|
|
343
|
+
return typeof days === "number" && Number.isFinite(days) && days >= 0 ? days : 30;
|
|
344
|
+
};
|
|
341
345
|
|
|
342
346
|
// src/services/jwt.ts
|
|
343
347
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -754,6 +758,27 @@ var UserModel = class {
|
|
|
754
758
|
);
|
|
755
759
|
return row ?? null;
|
|
756
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* An ARCHIVED (soft-deleted, not yet purged) account holding this address.
|
|
763
|
+
* Only for telling its owner — after they prove it is theirs — that it is
|
|
764
|
+
* scheduled for deletion, and for refusing to create a second account on
|
|
765
|
+
* the address meanwhile. Never for signing in.
|
|
766
|
+
*/
|
|
767
|
+
async findArchivedByEmail(email2) {
|
|
768
|
+
const [row] = await this.store.query(
|
|
769
|
+
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NOT NULL`,
|
|
770
|
+
[email2]
|
|
771
|
+
);
|
|
772
|
+
return row ?? null;
|
|
773
|
+
}
|
|
774
|
+
/** As findArchivedByEmail, for a phone number. */
|
|
775
|
+
async findArchivedByPhone(phone2) {
|
|
776
|
+
const [row] = await this.store.query(
|
|
777
|
+
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NOT NULL`,
|
|
778
|
+
[phone2]
|
|
779
|
+
);
|
|
780
|
+
return row ?? null;
|
|
781
|
+
}
|
|
757
782
|
async findByPhone(phone2) {
|
|
758
783
|
const [row] = await this.store.query(
|
|
759
784
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
|
|
@@ -1608,7 +1633,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1608
1633
|
// src/controllers/auth.controller.ts
|
|
1609
1634
|
import { randomBytes as randomBytes3, randomInt } from "crypto";
|
|
1610
1635
|
import { NOTIFICATION_EVENT as NOTIFICATION_EVENT2 } from "@fonderie/events";
|
|
1611
|
-
import { setApiResponse as
|
|
1636
|
+
import { setApiResponse as setApiResponse4, HTTP as HTTP4, background as background3, canonicalLocale, defineLocales } from "@fonderie/core";
|
|
1612
1637
|
|
|
1613
1638
|
// src/services/cooldown.ts
|
|
1614
1639
|
function checkCooldown(lastSentAt, cooldownMs) {
|
|
@@ -1619,6 +1644,28 @@ function checkCooldown(lastSentAt, cooldownMs) {
|
|
|
1619
1644
|
// src/controllers/auth.controller.ts
|
|
1620
1645
|
init_password();
|
|
1621
1646
|
|
|
1647
|
+
// src/services/pending-deletion.ts
|
|
1648
|
+
import { setApiResponse as setApiResponse3, HTTP as HTTP3 } from "@fonderie/core";
|
|
1649
|
+
function deletionDate(deletedAt, config) {
|
|
1650
|
+
return new Date(deletedAt.getTime() + deletionGracePeriodDays(config) * 24 * 60 * 60 * 1e3);
|
|
1651
|
+
}
|
|
1652
|
+
function pendingDeletionResponse(deletedAt, config) {
|
|
1653
|
+
const deleteOn = deletionDate(deletedAt, config);
|
|
1654
|
+
return setApiResponse3(
|
|
1655
|
+
HTTP3.FORBIDDEN,
|
|
1656
|
+
"ACCOUNT_PENDING_DELETION",
|
|
1657
|
+
`This account is scheduled for deletion on ${deleteOn.toISOString().slice(0, 10)}.`,
|
|
1658
|
+
{ requestedAt: deletedAt.toISOString(), deleteOn: deleteOn.toISOString() }
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
function archivedAddressResponse() {
|
|
1662
|
+
return setApiResponse3(
|
|
1663
|
+
HTTP3.CONFLICT,
|
|
1664
|
+
"ACCOUNT_PENDING_DELETION",
|
|
1665
|
+
"An account using this address is scheduled for deletion. Sign in to keep it, or wait until it is deleted."
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1622
1669
|
// src/models/password-reset.model.ts
|
|
1623
1670
|
import { createHash as createHash3 } from "crypto";
|
|
1624
1671
|
var hashSecret = (value) => createHash3("sha256").update(value).digest("hex");
|
|
@@ -1791,19 +1838,20 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1791
1838
|
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
1792
1839
|
const normalizedEmail = normalizeEmailSafe(email2);
|
|
1793
1840
|
if (!normalizedEmail) {
|
|
1794
|
-
return
|
|
1841
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1795
1842
|
}
|
|
1796
1843
|
if (password2.length < 8) {
|
|
1797
|
-
return
|
|
1798
|
-
|
|
1844
|
+
return setApiResponse4(
|
|
1845
|
+
HTTP4.UNPROCESSABLE,
|
|
1799
1846
|
"INVALID_PARAMETER",
|
|
1800
1847
|
"password must be at least 8 characters"
|
|
1801
1848
|
);
|
|
1802
1849
|
}
|
|
1803
1850
|
const existing = await users.findByEmail(normalizedEmail);
|
|
1804
1851
|
if (existing) {
|
|
1805
|
-
return
|
|
1852
|
+
return setApiResponse4(HTTP4.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
1806
1853
|
}
|
|
1854
|
+
if (await users.findArchivedByEmail(normalizedEmail)) return archivedAddressResponse();
|
|
1807
1855
|
const passwordHash = await hashPassword(password2);
|
|
1808
1856
|
const row = await users.create(
|
|
1809
1857
|
normalizedEmail,
|
|
@@ -1813,14 +1861,14 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1813
1861
|
locale
|
|
1814
1862
|
);
|
|
1815
1863
|
if (!row) {
|
|
1816
|
-
return
|
|
1864
|
+
return setApiResponse4(HTTP4.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1817
1865
|
}
|
|
1818
1866
|
const pin = randomInt(1e5, 1e6).toString();
|
|
1819
1867
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1820
1868
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1821
1869
|
const user = await users.findById(row.id);
|
|
1822
1870
|
if (!user) {
|
|
1823
|
-
return
|
|
1871
|
+
return setApiResponse4(HTTP4.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1824
1872
|
}
|
|
1825
1873
|
const reqId = ctx.meta["requestId"];
|
|
1826
1874
|
const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
|
|
@@ -1878,8 +1926,9 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1878
1926
|
if (isValidPhone(phone2)) {
|
|
1879
1927
|
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
1880
1928
|
if (existing) {
|
|
1881
|
-
return
|
|
1929
|
+
return setApiResponse4(HTTP4.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
1882
1930
|
}
|
|
1931
|
+
if (await users.findArchivedByPhone(normalizePhone(phone2))) return archivedAddressResponse();
|
|
1883
1932
|
const { id } = await users.findOrCreateByPhone(
|
|
1884
1933
|
normalizePhone(phone2),
|
|
1885
1934
|
firstName ?? null,
|
|
@@ -1891,7 +1940,7 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1891
1940
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1892
1941
|
const user = await users.findById(id);
|
|
1893
1942
|
if (!user) {
|
|
1894
|
-
return
|
|
1943
|
+
return setApiResponse4(HTTP4.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1895
1944
|
}
|
|
1896
1945
|
const reqId2 = ctx.meta["requestId"];
|
|
1897
1946
|
const reqOpts2 = reqId2 !== void 0 ? { requestId: reqId2 } : void 0;
|
|
@@ -1929,8 +1978,8 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1929
1978
|
{ status: 202 }
|
|
1930
1979
|
);
|
|
1931
1980
|
}
|
|
1932
|
-
return
|
|
1933
|
-
|
|
1981
|
+
return setApiResponse4(
|
|
1982
|
+
HTTP4.UNPROCESSABLE,
|
|
1934
1983
|
"INVALID_PARAMETER",
|
|
1935
1984
|
"Provide email + password or a valid phone number"
|
|
1936
1985
|
);
|
|
@@ -1942,9 +1991,25 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1942
1991
|
const { email: rawEmail, password: password2 } = body;
|
|
1943
1992
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
1944
1993
|
if (!email2) {
|
|
1945
|
-
return
|
|
1994
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1946
1995
|
}
|
|
1947
1996
|
const user = await users.findByEmail(email2);
|
|
1997
|
+
if (!user) {
|
|
1998
|
+
const archived = await users.findArchivedByEmail(email2);
|
|
1999
|
+
if (archived?.passwordHash && archived.deletedAt) {
|
|
2000
|
+
const { valid: valid2 } = await verifyPasswordForLogin(password2, archived.passwordHash, config.legacyVerify);
|
|
2001
|
+
await loginEvents.recordSafe({
|
|
2002
|
+
userId: archived.id,
|
|
2003
|
+
emailAttempted: email2,
|
|
2004
|
+
method: "password",
|
|
2005
|
+
outcome: "failed",
|
|
2006
|
+
failureReason: valid2 ? "pending_deletion" : "bad_password",
|
|
2007
|
+
...meta
|
|
2008
|
+
});
|
|
2009
|
+
if (valid2) return pendingDeletionResponse(archived.deletedAt, config);
|
|
2010
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1948
2013
|
if (!user || !user.passwordHash) {
|
|
1949
2014
|
await loginEvents.recordSafe({
|
|
1950
2015
|
userId: null,
|
|
@@ -1954,7 +2019,7 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1954
2019
|
failureReason: "unknown_email",
|
|
1955
2020
|
...meta
|
|
1956
2021
|
});
|
|
1957
|
-
return
|
|
2022
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1958
2023
|
}
|
|
1959
2024
|
const { valid, needsRehash } = await verifyPasswordForLogin(
|
|
1960
2025
|
password2,
|
|
@@ -1970,21 +2035,21 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1970
2035
|
failureReason: "bad_password",
|
|
1971
2036
|
...meta
|
|
1972
2037
|
});
|
|
1973
|
-
return
|
|
2038
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1974
2039
|
}
|
|
1975
2040
|
if (needsRehash) {
|
|
1976
2041
|
await users.updatePassword(user.id, await hashPassword(password2));
|
|
1977
2042
|
}
|
|
1978
2043
|
if (user.suspended) {
|
|
1979
|
-
return
|
|
1980
|
-
|
|
2044
|
+
return setApiResponse4(
|
|
2045
|
+
HTTP4.FORBIDDEN,
|
|
1981
2046
|
"ACCOUNT_SUSPENDED",
|
|
1982
2047
|
"Account suspended. Please contact support."
|
|
1983
2048
|
);
|
|
1984
2049
|
}
|
|
1985
2050
|
if (user.mfaEnabled) {
|
|
1986
2051
|
const mfaToken = issueMfaPendingToken(user.id, config, "email");
|
|
1987
|
-
return
|
|
2052
|
+
return setApiResponse4(HTTP4.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
|
|
1988
2053
|
mfaToken
|
|
1989
2054
|
});
|
|
1990
2055
|
}
|
|
@@ -2021,11 +2086,11 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2021
2086
|
if (isValidPhone(phone2)) {
|
|
2022
2087
|
const user = await users.findByPhone(normalizePhone(phone2));
|
|
2023
2088
|
if (!user) {
|
|
2024
|
-
return
|
|
2089
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2025
2090
|
}
|
|
2026
2091
|
if (user.suspended) {
|
|
2027
|
-
return
|
|
2028
|
-
|
|
2092
|
+
return setApiResponse4(
|
|
2093
|
+
HTTP4.FORBIDDEN,
|
|
2029
2094
|
"ACCOUNT_SUSPENDED",
|
|
2030
2095
|
"Account suspended. Please contact support."
|
|
2031
2096
|
);
|
|
@@ -2049,8 +2114,8 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2049
2114
|
{ status: 202 }
|
|
2050
2115
|
);
|
|
2051
2116
|
}
|
|
2052
|
-
return
|
|
2053
|
-
|
|
2117
|
+
return setApiResponse4(
|
|
2118
|
+
HTTP4.UNPROCESSABLE,
|
|
2054
2119
|
"INVALID_PARAMETER",
|
|
2055
2120
|
"Provide email + password or a valid phone number"
|
|
2056
2121
|
);
|
|
@@ -2075,35 +2140,35 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2075
2140
|
refresh: async (ctx) => {
|
|
2076
2141
|
const token = extractRefreshToken(ctx);
|
|
2077
2142
|
if (!token) {
|
|
2078
|
-
return
|
|
2143
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
|
|
2079
2144
|
}
|
|
2080
2145
|
const payload = verifyToken(token, config);
|
|
2081
2146
|
if (!payload || payload.type !== "refresh") {
|
|
2082
|
-
return
|
|
2147
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
2083
2148
|
}
|
|
2084
2149
|
const runtime = config.resolve?.(ctx);
|
|
2085
2150
|
let issued = null;
|
|
2086
2151
|
for (let attempt = 0; attempt < 3 && !issued; attempt++) {
|
|
2087
2152
|
const found = await sessions.match(token);
|
|
2088
2153
|
if (found.kind === "unknown") {
|
|
2089
|
-
return
|
|
2154
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2090
2155
|
}
|
|
2091
2156
|
if (found.kind === "reused") {
|
|
2092
2157
|
await sessions.revokeById(found.row.id);
|
|
2093
2158
|
await background3(
|
|
2094
2159
|
bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sids: [found.row.sid], reason: "refresh-reuse" })
|
|
2095
2160
|
);
|
|
2096
|
-
return
|
|
2161
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2097
2162
|
}
|
|
2098
2163
|
const resolvedRefresh = configForClient(config, runtime, found.row.clientKind);
|
|
2099
2164
|
const maxAgeMs = resolvedRefresh.sessionMaxAge ? durationMs(String(resolvedRefresh.sessionMaxAge)) : null;
|
|
2100
2165
|
if (maxAgeMs !== null && Date.now() - found.row.createdAt.getTime() > maxAgeMs) {
|
|
2101
2166
|
await sessions.revokeById(found.row.id);
|
|
2102
|
-
return
|
|
2167
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
|
|
2103
2168
|
}
|
|
2104
2169
|
const user = await users.findById(found.row.userId);
|
|
2105
2170
|
if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
|
|
2106
|
-
return
|
|
2171
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
2107
2172
|
}
|
|
2108
2173
|
const pair = issueTokenPair(user.id, resolvedRefresh, {
|
|
2109
2174
|
loginMethod: payload.loginMethod ?? "email",
|
|
@@ -2116,7 +2181,7 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2116
2181
|
if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
|
|
2117
2182
|
}
|
|
2118
2183
|
if (!issued) {
|
|
2119
|
-
return
|
|
2184
|
+
return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2120
2185
|
}
|
|
2121
2186
|
const { accessToken, refreshToken } = issued;
|
|
2122
2187
|
return Response.json(
|
|
@@ -2135,16 +2200,16 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2135
2200
|
const body = ctx.meta["body"];
|
|
2136
2201
|
const rawEmail = body?.["email"];
|
|
2137
2202
|
if (typeof rawEmail !== "string") {
|
|
2138
|
-
return
|
|
2203
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
2139
2204
|
}
|
|
2140
2205
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
2141
2206
|
if (!email2) {
|
|
2142
|
-
return
|
|
2207
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
2143
2208
|
}
|
|
2144
2209
|
const user = await users.findByEmail(email2);
|
|
2145
2210
|
if (!user) {
|
|
2146
|
-
return
|
|
2147
|
-
|
|
2211
|
+
return setApiResponse4(
|
|
2212
|
+
HTTP4.OK,
|
|
2148
2213
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
2149
2214
|
"Password reset email sent (if account exists)."
|
|
2150
2215
|
);
|
|
@@ -2153,8 +2218,8 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2153
2218
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
2154
2219
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
2155
2220
|
if (remaining > 0) {
|
|
2156
|
-
return
|
|
2157
|
-
|
|
2221
|
+
return setApiResponse4(
|
|
2222
|
+
HTTP4.OK,
|
|
2158
2223
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
2159
2224
|
"Password reset email sent (if account exists)."
|
|
2160
2225
|
);
|
|
@@ -2171,8 +2236,8 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2171
2236
|
recipient: { email: email2, phone: null, deviceToken: null },
|
|
2172
2237
|
data: { pin, token, resetUrl }
|
|
2173
2238
|
}));
|
|
2174
|
-
return
|
|
2175
|
-
|
|
2239
|
+
return setApiResponse4(
|
|
2240
|
+
HTTP4.OK,
|
|
2176
2241
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
2177
2242
|
"Password reset email sent (if account exists)."
|
|
2178
2243
|
);
|
|
@@ -2183,11 +2248,11 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2183
2248
|
const password2 = body?.["password"];
|
|
2184
2249
|
const rawToken = body?.["token"];
|
|
2185
2250
|
if (typeof password2 !== "string") {
|
|
2186
|
-
return
|
|
2251
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
|
|
2187
2252
|
}
|
|
2188
2253
|
if (password2.length < 8) {
|
|
2189
|
-
return
|
|
2190
|
-
|
|
2254
|
+
return setApiResponse4(
|
|
2255
|
+
HTTP4.UNPROCESSABLE,
|
|
2191
2256
|
"INVALID_PARAMETER",
|
|
2192
2257
|
"password must be at least 8 characters"
|
|
2193
2258
|
);
|
|
@@ -2197,8 +2262,8 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2197
2262
|
row = await passwordReset.findByToken(rawToken.trim());
|
|
2198
2263
|
} else {
|
|
2199
2264
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
2200
|
-
return
|
|
2201
|
-
|
|
2265
|
+
return setApiResponse4(
|
|
2266
|
+
HTTP4.UNPROCESSABLE,
|
|
2202
2267
|
"INVALID_PARAMETER",
|
|
2203
2268
|
"a 6-digit pin or a reset token is required"
|
|
2204
2269
|
);
|
|
@@ -2206,33 +2271,41 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2206
2271
|
row = await passwordReset.findByPin(raw.trim());
|
|
2207
2272
|
}
|
|
2208
2273
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
2209
|
-
return
|
|
2210
|
-
|
|
2274
|
+
return setApiResponse4(
|
|
2275
|
+
HTTP4.BAD_REQUEST,
|
|
2211
2276
|
"PASSWORD_RESET_FAILED",
|
|
2212
2277
|
"Invalid or expired reset credentials"
|
|
2213
2278
|
);
|
|
2214
2279
|
}
|
|
2215
2280
|
const passwordHash = await hashPassword(password2);
|
|
2216
|
-
await store.
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2281
|
+
const [changed] = await store.query(
|
|
2282
|
+
`WITH changed AS (
|
|
2283
|
+
UPDATE fonderie_users SET password_hash = $1, updated_at = now()
|
|
2284
|
+
WHERE id = $2 AND deleted_at IS NULL
|
|
2285
|
+
RETURNING id
|
|
2286
|
+
), spent AS (
|
|
2287
|
+
DELETE FROM fonderie_password_resets WHERE user_id = $2
|
|
2288
|
+
), revoked AS (
|
|
2289
|
+
DELETE FROM fonderie_sessions WHERE user_id IN (SELECT id FROM changed)
|
|
2290
|
+
)
|
|
2291
|
+
SELECT id FROM changed`,
|
|
2292
|
+
[passwordHash, row.userId]
|
|
2293
|
+
);
|
|
2294
|
+
if (!changed) {
|
|
2295
|
+
return setApiResponse4(
|
|
2296
|
+
HTTP4.BAD_REQUEST,
|
|
2297
|
+
"PASSWORD_RESET_FAILED",
|
|
2298
|
+
"Invalid or expired reset credentials"
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
return setApiResponse4(HTTP4.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
|
|
2229
2302
|
},
|
|
2230
2303
|
verify: async (ctx) => {
|
|
2231
2304
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
2232
|
-
return
|
|
2305
|
+
return setApiResponse4(HTTP4.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
2233
2306
|
}
|
|
2234
2307
|
if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
|
|
2235
|
-
return
|
|
2308
|
+
return setApiResponse4(HTTP4.OK, "VERIFIED", "Email verified successfully.", {
|
|
2236
2309
|
verified: true,
|
|
2237
2310
|
email: ctx.user.email
|
|
2238
2311
|
});
|
|
@@ -2240,8 +2313,8 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2240
2313
|
const body = ctx.meta["body"];
|
|
2241
2314
|
const raw = body?.["token"];
|
|
2242
2315
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
2243
|
-
return
|
|
2244
|
-
|
|
2316
|
+
return setApiResponse4(
|
|
2317
|
+
HTTP4.UNPROCESSABLE,
|
|
2245
2318
|
"INVALID_PARAMETER",
|
|
2246
2319
|
"token must be a 6-digit code"
|
|
2247
2320
|
);
|
|
@@ -2259,7 +2332,7 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2259
2332
|
failureReason: "invalid_pin",
|
|
2260
2333
|
...phoneMeta
|
|
2261
2334
|
});
|
|
2262
|
-
return
|
|
2335
|
+
return setApiResponse4(HTTP4.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
2263
2336
|
}
|
|
2264
2337
|
if (/* @__PURE__ */ new Date() > record.expiresAt) {
|
|
2265
2338
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
@@ -2271,8 +2344,8 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2271
2344
|
failureReason: "expired_pin",
|
|
2272
2345
|
...phoneMeta
|
|
2273
2346
|
});
|
|
2274
|
-
return
|
|
2275
|
-
|
|
2347
|
+
return setApiResponse4(
|
|
2348
|
+
HTTP4.BAD_REQUEST,
|
|
2276
2349
|
"VERIFICATION_FAILED",
|
|
2277
2350
|
"Verification code expired"
|
|
2278
2351
|
);
|
|
@@ -2292,11 +2365,11 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2292
2365
|
});
|
|
2293
2366
|
const verifiedUser = await users.findById(ctx.user.id);
|
|
2294
2367
|
if (!verifiedUser) {
|
|
2295
|
-
return
|
|
2368
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2296
2369
|
}
|
|
2297
2370
|
if (verifiedUser.suspended) {
|
|
2298
|
-
return
|
|
2299
|
-
|
|
2371
|
+
return setApiResponse4(
|
|
2372
|
+
HTTP4.FORBIDDEN,
|
|
2300
2373
|
"ACCOUNT_SUSPENDED",
|
|
2301
2374
|
"Account suspended. Please contact support."
|
|
2302
2375
|
);
|
|
@@ -2318,10 +2391,10 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2318
2391
|
}
|
|
2319
2392
|
const row = await emailVerif.findByUser(ctx.user.id, pin);
|
|
2320
2393
|
if (!row) {
|
|
2321
|
-
return
|
|
2394
|
+
return setApiResponse4(HTTP4.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
2322
2395
|
}
|
|
2323
2396
|
if (/* @__PURE__ */ new Date() > row.expiresAt) {
|
|
2324
|
-
return
|
|
2397
|
+
return setApiResponse4(HTTP4.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
|
|
2325
2398
|
}
|
|
2326
2399
|
await store.transaction(async (tx) => {
|
|
2327
2400
|
await Promise.all([
|
|
@@ -2335,30 +2408,30 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2335
2408
|
])
|
|
2336
2409
|
]);
|
|
2337
2410
|
});
|
|
2338
|
-
return
|
|
2411
|
+
return setApiResponse4(HTTP4.OK, "VERIFIED", "Email verified successfully.", {
|
|
2339
2412
|
verified: true,
|
|
2340
2413
|
email: ctx.user.email
|
|
2341
2414
|
});
|
|
2342
2415
|
},
|
|
2343
2416
|
sendVerification: async (ctx) => {
|
|
2344
2417
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
2345
|
-
return
|
|
2418
|
+
return setApiResponse4(HTTP4.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
2346
2419
|
}
|
|
2347
2420
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
2348
2421
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
2349
2422
|
if (ctx.user.loginMethod === "phone") {
|
|
2350
2423
|
const phone2 = ctx.user.phone;
|
|
2351
2424
|
if (!phone2) {
|
|
2352
|
-
return
|
|
2353
|
-
|
|
2425
|
+
return setApiResponse4(
|
|
2426
|
+
HTTP4.BAD_REQUEST,
|
|
2354
2427
|
"NO_PHONE_ON_ACCOUNT",
|
|
2355
2428
|
"No phone number associated with this account"
|
|
2356
2429
|
);
|
|
2357
2430
|
}
|
|
2358
2431
|
const remaining2 = checkCooldown(await phoneVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
2359
2432
|
if (remaining2 > 0) {
|
|
2360
|
-
return
|
|
2361
|
-
|
|
2433
|
+
return setApiResponse4(
|
|
2434
|
+
HTTP4.TOO_MANY_REQUESTS,
|
|
2362
2435
|
"VERIFICATION_COOLDOWN",
|
|
2363
2436
|
"Please wait before requesting a new code.",
|
|
2364
2437
|
{
|
|
@@ -2375,29 +2448,29 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2375
2448
|
data: { otp },
|
|
2376
2449
|
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
2377
2450
|
}));
|
|
2378
|
-
return
|
|
2379
|
-
|
|
2451
|
+
return setApiResponse4(
|
|
2452
|
+
HTTP4.OK,
|
|
2380
2453
|
"VERIFICATION_SENT",
|
|
2381
2454
|
"A verification code has been sent to your phone."
|
|
2382
2455
|
);
|
|
2383
2456
|
}
|
|
2384
2457
|
if (!ctx.user.email) {
|
|
2385
|
-
return
|
|
2386
|
-
|
|
2458
|
+
return setApiResponse4(
|
|
2459
|
+
HTTP4.BAD_REQUEST,
|
|
2387
2460
|
"NO_EMAIL_ON_ACCOUNT",
|
|
2388
2461
|
"No email address associated with this account"
|
|
2389
2462
|
);
|
|
2390
2463
|
}
|
|
2391
2464
|
if (ctx.user.emailVerifiedAt) {
|
|
2392
|
-
return
|
|
2465
|
+
return setApiResponse4(HTTP4.OK, "EMAIL_VERIFIED", "Email already verified.", {
|
|
2393
2466
|
verified: true,
|
|
2394
2467
|
email: ctx.user.email
|
|
2395
2468
|
});
|
|
2396
2469
|
}
|
|
2397
2470
|
const remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
2398
2471
|
if (remaining > 0) {
|
|
2399
|
-
return
|
|
2400
|
-
|
|
2472
|
+
return setApiResponse4(
|
|
2473
|
+
HTTP4.TOO_MANY_REQUESTS,
|
|
2401
2474
|
"VERIFICATION_COOLDOWN",
|
|
2402
2475
|
"Please wait before requesting a new code.",
|
|
2403
2476
|
{
|
|
@@ -2414,7 +2487,7 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2414
2487
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null },
|
|
2415
2488
|
data: { pin }
|
|
2416
2489
|
}));
|
|
2417
|
-
return
|
|
2490
|
+
return setApiResponse4(HTTP4.OK, "VERIFICATION_SENT", "Verification email sent.", {
|
|
2418
2491
|
email: ctx.user.email
|
|
2419
2492
|
});
|
|
2420
2493
|
}
|
|
@@ -2423,7 +2496,7 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2423
2496
|
|
|
2424
2497
|
// src/controllers/user.controller.ts
|
|
2425
2498
|
import { randomInt as randomInt2 } from "crypto";
|
|
2426
|
-
import { setApiResponse as
|
|
2499
|
+
import { setApiResponse as setApiResponse5, HTTP as HTTP5, dateOrEmpty as dateOrEmpty2, background as background4 } from "@fonderie/core";
|
|
2427
2500
|
import { NOTIFICATION_EVENT as NOTIFICATION_EVENT3 } from "@fonderie/events";
|
|
2428
2501
|
|
|
2429
2502
|
// src/dtos/login-activity.ts
|
|
@@ -2478,9 +2551,9 @@ function userController(store, config, bus) {
|
|
|
2478
2551
|
me: async (ctx) => {
|
|
2479
2552
|
const user = await users.findById(ctx.user.id);
|
|
2480
2553
|
if (!user) {
|
|
2481
|
-
return
|
|
2554
|
+
return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2482
2555
|
}
|
|
2483
|
-
return
|
|
2556
|
+
return setApiResponse5(HTTP5.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
|
|
2484
2557
|
user: toUserDTO(user, ctx.user.phoneVerified)
|
|
2485
2558
|
});
|
|
2486
2559
|
},
|
|
@@ -2491,14 +2564,14 @@ function userController(store, config, bus) {
|
|
|
2491
2564
|
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
2492
2565
|
const outcomeParam = params.get("outcome");
|
|
2493
2566
|
if (outcomeParam !== null && outcomeParam !== "success" && outcomeParam !== "failed") {
|
|
2494
|
-
return
|
|
2567
|
+
return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
|
|
2495
2568
|
}
|
|
2496
2569
|
const cursorParam = params.get("cursor");
|
|
2497
2570
|
let cursor = null;
|
|
2498
2571
|
if (cursorParam) {
|
|
2499
2572
|
cursor = decodeLoginCursor(cursorParam);
|
|
2500
2573
|
if (!cursor) {
|
|
2501
|
-
return
|
|
2574
|
+
return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
2502
2575
|
}
|
|
2503
2576
|
}
|
|
2504
2577
|
const query = { userId: ctx.user.id, limit };
|
|
@@ -2509,8 +2582,8 @@ function userController(store, config, bus) {
|
|
|
2509
2582
|
if (from) query.from = new Date(from);
|
|
2510
2583
|
if (to) query.to = new Date(to);
|
|
2511
2584
|
const page = await loginEvents.listByUser(query);
|
|
2512
|
-
return
|
|
2513
|
-
|
|
2585
|
+
return setApiResponse5(
|
|
2586
|
+
HTTP5.OK,
|
|
2514
2587
|
"LOGIN_HISTORY_FETCHED",
|
|
2515
2588
|
"Login history retrieved.",
|
|
2516
2589
|
toLoginHistoryPageDTO(page)
|
|
@@ -2520,27 +2593,27 @@ function userController(store, config, bus) {
|
|
|
2520
2593
|
listSessions: async (ctx) => {
|
|
2521
2594
|
const rows = await sessions.listLiveByUser(ctx.user.id);
|
|
2522
2595
|
const currentSid = ctx.user.sid ?? null;
|
|
2523
|
-
return
|
|
2596
|
+
return setApiResponse5(HTTP5.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
|
|
2524
2597
|
sessions: rows.map((r) => toSessionDTO(r, currentSid))
|
|
2525
2598
|
});
|
|
2526
2599
|
},
|
|
2527
2600
|
terminateSession: async (ctx) => {
|
|
2528
2601
|
const id = ctx.meta["params"]?.["id"];
|
|
2529
2602
|
if (!id) {
|
|
2530
|
-
return
|
|
2603
|
+
return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2531
2604
|
}
|
|
2532
2605
|
const removed = await sessions.terminateByIdReturningSid(ctx.user.id, id);
|
|
2533
2606
|
if (removed === void 0) {
|
|
2534
|
-
return
|
|
2607
|
+
return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2535
2608
|
}
|
|
2536
2609
|
await background4(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: [removed], reason: "terminated" }));
|
|
2537
|
-
return
|
|
2610
|
+
return setApiResponse5(HTTP5.OK, "SESSION_TERMINATED", "Session terminated.", { id });
|
|
2538
2611
|
},
|
|
2539
2612
|
terminateOtherSessions: async (ctx) => {
|
|
2540
2613
|
const currentSid = ctx.user.sid ?? null;
|
|
2541
2614
|
if (!currentSid) {
|
|
2542
|
-
return
|
|
2543
|
-
|
|
2615
|
+
return setApiResponse5(
|
|
2616
|
+
HTTP5.UNPROCESSABLE,
|
|
2544
2617
|
"NO_CURRENT_SESSION",
|
|
2545
2618
|
"Cannot terminate others without a current session context"
|
|
2546
2619
|
);
|
|
@@ -2548,7 +2621,7 @@ function userController(store, config, bus) {
|
|
|
2548
2621
|
const sids = await sessions.terminateOthersReturningSids(ctx.user.id, currentSid);
|
|
2549
2622
|
const count = sids.length;
|
|
2550
2623
|
if (count) await background4(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids, reason: "terminated" }));
|
|
2551
|
-
return
|
|
2624
|
+
return setApiResponse5(HTTP5.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
|
|
2552
2625
|
},
|
|
2553
2626
|
updateProfile: async (ctx) => {
|
|
2554
2627
|
const body = ctx.meta["body"];
|
|
@@ -2558,18 +2631,18 @@ function userController(store, config, bus) {
|
|
|
2558
2631
|
if (body?.[key] !== void 0) fields[key] = body[key];
|
|
2559
2632
|
}
|
|
2560
2633
|
if (Object.keys(fields).length === 0) {
|
|
2561
|
-
return
|
|
2562
|
-
|
|
2634
|
+
return setApiResponse5(
|
|
2635
|
+
HTTP5.UNPROCESSABLE,
|
|
2563
2636
|
"INVALID_PARAMETER",
|
|
2564
2637
|
"Provide at least one of: firstName, lastName, avatarUrl"
|
|
2565
2638
|
);
|
|
2566
2639
|
}
|
|
2567
2640
|
const row = await users.update(ctx.user.id, fields);
|
|
2568
2641
|
if (!row) {
|
|
2569
|
-
return
|
|
2642
|
+
return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2570
2643
|
}
|
|
2571
2644
|
const updated = await users.findById(ctx.user.id);
|
|
2572
|
-
return
|
|
2645
|
+
return setApiResponse5(HTTP5.OK, "PROFILE_UPDATED", "Profile updated.", {
|
|
2573
2646
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2574
2647
|
});
|
|
2575
2648
|
},
|
|
@@ -2586,14 +2659,14 @@ function userController(store, config, bus) {
|
|
|
2586
2659
|
if (Object.keys(patch).length > 0) fields.patch = patch;
|
|
2587
2660
|
const row = await users.updatePreferences(ctx.user.id, fields);
|
|
2588
2661
|
if (!row) {
|
|
2589
|
-
return
|
|
2590
|
-
|
|
2662
|
+
return setApiResponse5(
|
|
2663
|
+
HTTP5.UNPROCESSABLE,
|
|
2591
2664
|
"INVALID_PARAMETER",
|
|
2592
2665
|
"Provide at least one preference field"
|
|
2593
2666
|
);
|
|
2594
2667
|
}
|
|
2595
2668
|
const updated = await users.findById(ctx.user.id);
|
|
2596
|
-
return
|
|
2669
|
+
return setApiResponse5(HTTP5.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
|
|
2597
2670
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2598
2671
|
});
|
|
2599
2672
|
},
|
|
@@ -2602,23 +2675,23 @@ function userController(store, config, bus) {
|
|
|
2602
2675
|
const newEmail = body?.["email"];
|
|
2603
2676
|
const normalised = typeof newEmail === "string" ? normalizeEmailSafe(newEmail) : null;
|
|
2604
2677
|
if (!normalised) {
|
|
2605
|
-
return
|
|
2606
|
-
|
|
2678
|
+
return setApiResponse5(
|
|
2679
|
+
HTTP5.UNPROCESSABLE,
|
|
2607
2680
|
"INVALID_PARAMETER",
|
|
2608
2681
|
"A valid email address is required"
|
|
2609
2682
|
);
|
|
2610
2683
|
}
|
|
2611
2684
|
const oldEmail = ctx.user.email;
|
|
2612
2685
|
if (normalised === oldEmail) {
|
|
2613
|
-
return
|
|
2614
|
-
|
|
2686
|
+
return setApiResponse5(
|
|
2687
|
+
HTTP5.UNPROCESSABLE,
|
|
2615
2688
|
"INVALID_PARAMETER",
|
|
2616
2689
|
"New email must differ from current email"
|
|
2617
2690
|
);
|
|
2618
2691
|
}
|
|
2619
2692
|
const existing = await users.findByEmail(normalised);
|
|
2620
2693
|
if (existing) {
|
|
2621
|
-
return
|
|
2694
|
+
return setApiResponse5(HTTP5.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
2622
2695
|
}
|
|
2623
2696
|
const pin = randomInt2(1e5, 1e6).toString();
|
|
2624
2697
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
@@ -2638,8 +2711,8 @@ function userController(store, config, bus) {
|
|
|
2638
2711
|
recipient: { email: oldEmail, phone: null, deviceToken: null }
|
|
2639
2712
|
}));
|
|
2640
2713
|
}
|
|
2641
|
-
return
|
|
2642
|
-
|
|
2714
|
+
return setApiResponse5(
|
|
2715
|
+
HTTP5.OK,
|
|
2643
2716
|
"EMAIL_UPDATED",
|
|
2644
2717
|
"Email updated. A verification code has been sent to your new address.",
|
|
2645
2718
|
{
|
|
@@ -2651,8 +2724,8 @@ function userController(store, config, bus) {
|
|
|
2651
2724
|
const body = ctx.meta["body"];
|
|
2652
2725
|
const newPhone = body?.["phone"];
|
|
2653
2726
|
if (!isValidPhone2(newPhone)) {
|
|
2654
|
-
return
|
|
2655
|
-
|
|
2727
|
+
return setApiResponse5(
|
|
2728
|
+
HTTP5.UNPROCESSABLE,
|
|
2656
2729
|
"INVALID_PARAMETER",
|
|
2657
2730
|
"A valid phone number is required"
|
|
2658
2731
|
);
|
|
@@ -2660,7 +2733,7 @@ function userController(store, config, bus) {
|
|
|
2660
2733
|
const normalised = normalizePhone2(newPhone);
|
|
2661
2734
|
const existing = await users.findByPhone(normalised);
|
|
2662
2735
|
if (existing) {
|
|
2663
|
-
return
|
|
2736
|
+
return setApiResponse5(HTTP5.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
2664
2737
|
}
|
|
2665
2738
|
const otp = randomInt2(1e5, 1e6).toString();
|
|
2666
2739
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
@@ -2680,8 +2753,8 @@ function userController(store, config, bus) {
|
|
|
2680
2753
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
2681
2754
|
}));
|
|
2682
2755
|
}
|
|
2683
|
-
return
|
|
2684
|
-
|
|
2756
|
+
return setApiResponse5(
|
|
2757
|
+
HTTP5.OK,
|
|
2685
2758
|
"PHONE_UPDATED",
|
|
2686
2759
|
"Phone number updated. A verification code has been sent to your new number.",
|
|
2687
2760
|
{
|
|
@@ -2694,23 +2767,23 @@ function userController(store, config, bus) {
|
|
|
2694
2767
|
const currentPassword = body?.["currentPassword"];
|
|
2695
2768
|
const newPassword = body?.["newPassword"];
|
|
2696
2769
|
if (typeof currentPassword !== "string" || !currentPassword) {
|
|
2697
|
-
return
|
|
2770
|
+
return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
|
|
2698
2771
|
}
|
|
2699
2772
|
if (typeof newPassword !== "string" || newPassword.length < 8) {
|
|
2700
|
-
return
|
|
2773
|
+
return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
|
|
2701
2774
|
}
|
|
2702
2775
|
const user = await users.findById(ctx.user.id);
|
|
2703
|
-
if (!user) return
|
|
2776
|
+
if (!user) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2704
2777
|
const { hashPassword: hashPassword2, verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password(), password_exports));
|
|
2705
2778
|
const valid = await verifyPassword2(currentPassword, user.passwordHash ?? "");
|
|
2706
2779
|
if (!valid) {
|
|
2707
|
-
return
|
|
2780
|
+
return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
|
|
2708
2781
|
}
|
|
2709
2782
|
const hash = await hashPassword2(newPassword);
|
|
2710
2783
|
await users.updatePassword(ctx.user.id, hash);
|
|
2711
2784
|
await sessions.deleteByUser(ctx.user.id);
|
|
2712
2785
|
await background4(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: null, reason: "password-changed" }));
|
|
2713
|
-
return
|
|
2786
|
+
return setApiResponse5(HTTP5.OK, "PASSWORD_CHANGED", "Password updated successfully.");
|
|
2714
2787
|
},
|
|
2715
2788
|
/**
|
|
2716
2789
|
* Remove an OAuth provider from the account.
|
|
@@ -2729,19 +2802,19 @@ function userController(store, config, bus) {
|
|
|
2729
2802
|
const provider = String(ctx.meta["params"]?.["provider"] ?? "");
|
|
2730
2803
|
const current = await users.findById(ctx.user.id);
|
|
2731
2804
|
if (!current?.provider) {
|
|
2732
|
-
return
|
|
2805
|
+
return setApiResponse5(HTTP5.NOT_FOUND, "NOT_LINKED", "No sign-in provider is linked.");
|
|
2733
2806
|
}
|
|
2734
2807
|
if (provider && provider.toLowerCase() !== current.provider.toLowerCase()) {
|
|
2735
|
-
return
|
|
2736
|
-
|
|
2808
|
+
return setApiResponse5(
|
|
2809
|
+
HTTP5.NOT_FOUND,
|
|
2737
2810
|
"NOT_LINKED",
|
|
2738
2811
|
`This account is not linked to ${provider}.`
|
|
2739
2812
|
);
|
|
2740
2813
|
}
|
|
2741
2814
|
const removed = await users.clearProvider(ctx.user.id);
|
|
2742
2815
|
if (!removed) {
|
|
2743
|
-
return
|
|
2744
|
-
|
|
2816
|
+
return setApiResponse5(
|
|
2817
|
+
HTTP5.CONFLICT,
|
|
2745
2818
|
"PASSWORD_REQUIRED",
|
|
2746
2819
|
"Set a password before disconnecting this provider \u2014 it is currently the only way to sign in."
|
|
2747
2820
|
);
|
|
@@ -2754,12 +2827,18 @@ function userController(store, config, bus) {
|
|
|
2754
2827
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
2755
2828
|
}));
|
|
2756
2829
|
}
|
|
2757
|
-
return
|
|
2830
|
+
return setApiResponse5(HTTP5.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
|
|
2758
2831
|
},
|
|
2759
2832
|
deleteMe: async (ctx) => {
|
|
2760
2833
|
const userId = ctx.user.id;
|
|
2761
2834
|
await users.softDelete(userId);
|
|
2762
2835
|
await sessions.deleteByUser(userId);
|
|
2836
|
+
await store.query(
|
|
2837
|
+
`WITH resets AS (DELETE FROM fonderie_password_resets WHERE user_id = $1),
|
|
2838
|
+
emails AS (DELETE FROM fonderie_email_verifications WHERE user_id = $1)
|
|
2839
|
+
DELETE FROM fonderie_phone_verifications WHERE user_id = $1`,
|
|
2840
|
+
[userId]
|
|
2841
|
+
);
|
|
2763
2842
|
const reqId = ctx.meta["requestId"];
|
|
2764
2843
|
await background4(bus?.emit(
|
|
2765
2844
|
EVENT_KEYS.userDeleted,
|
|
@@ -2780,7 +2859,7 @@ function userController(store, config, bus) {
|
|
|
2780
2859
|
// no MFA secret, no session tokens (session metadata only).
|
|
2781
2860
|
exportMe: async (ctx) => {
|
|
2782
2861
|
const user = await users.findById(ctx.user.id);
|
|
2783
|
-
if (!user) return
|
|
2862
|
+
if (!user) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2784
2863
|
const sessionRows = await sessions.listByUser(ctx.user.id);
|
|
2785
2864
|
const bundle = {
|
|
2786
2865
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2809,7 +2888,7 @@ function userController(store, config, bus) {
|
|
|
2809
2888
|
}
|
|
2810
2889
|
bundle["modules"] = modules;
|
|
2811
2890
|
}
|
|
2812
|
-
return
|
|
2891
|
+
return setApiResponse5(HTTP5.OK, "DATA_EXPORT", "Your account data.", bundle);
|
|
2813
2892
|
}
|
|
2814
2893
|
};
|
|
2815
2894
|
}
|
|
@@ -2817,7 +2896,7 @@ function userController(store, config, bus) {
|
|
|
2817
2896
|
// src/controllers/oauth.controller.ts
|
|
2818
2897
|
import { randomBytes as randomBytes4, createHash as createHash4 } from "crypto";
|
|
2819
2898
|
import jwt2 from "jsonwebtoken";
|
|
2820
|
-
import { setApiResponse as
|
|
2899
|
+
import { setApiResponse as setApiResponse6, HTTP as HTTP6, constantTimeEqual as constantTimeEqual2, background as background5 } from "@fonderie/core";
|
|
2821
2900
|
import { NOTIFICATION_EVENT as NOTIFICATION_EVENT4 } from "@fonderie/events";
|
|
2822
2901
|
|
|
2823
2902
|
// src/services/jwks.ts
|
|
@@ -3027,14 +3106,29 @@ function oauthController(store, config, bus) {
|
|
|
3027
3106
|
const method = provider === "apple" ? "oauth-apple" : "oauth-google";
|
|
3028
3107
|
const meta = requestMeta(ctx);
|
|
3029
3108
|
if (!claims.email) {
|
|
3030
|
-
return
|
|
3109
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, p.failed, `No email in ${p.label} identity token`);
|
|
3031
3110
|
}
|
|
3032
3111
|
if (!claims.emailVerified) {
|
|
3033
|
-
return
|
|
3112
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, p.failed, `${p.label} account email is not verified`);
|
|
3034
3113
|
}
|
|
3035
3114
|
const normalizedEmail = normalizeEmailSafe(claims.email);
|
|
3036
3115
|
if (!normalizedEmail) {
|
|
3037
|
-
return
|
|
3116
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, p.failed, `Invalid email in ${p.label} identity token`);
|
|
3117
|
+
}
|
|
3118
|
+
const archived = await users.findArchivedByEmail(normalizedEmail);
|
|
3119
|
+
if (archived?.deletedAt) {
|
|
3120
|
+
await loginEvents.recordSafe({
|
|
3121
|
+
userId: archived.id,
|
|
3122
|
+
emailAttempted: normalizedEmail,
|
|
3123
|
+
method,
|
|
3124
|
+
outcome: "failed",
|
|
3125
|
+
failureReason: "pending_deletion",
|
|
3126
|
+
...meta
|
|
3127
|
+
});
|
|
3128
|
+
const res = pendingDeletionResponse(archived.deletedAt, config);
|
|
3129
|
+
const headers = cookieHeaders(clearCookies);
|
|
3130
|
+
headers.set("content-type", res.headers.get("content-type") ?? "application/json");
|
|
3131
|
+
return new Response(res.body, { status: res.status, headers });
|
|
3038
3132
|
}
|
|
3039
3133
|
const upserted = await users.upsertByProvider(normalizedEmail, provider, claims.sub ?? "");
|
|
3040
3134
|
if (!upserted) {
|
|
@@ -3046,15 +3140,15 @@ function oauthController(store, config, bus) {
|
|
|
3046
3140
|
failureReason: "provider_upsert_failed",
|
|
3047
3141
|
...meta
|
|
3048
3142
|
});
|
|
3049
|
-
return
|
|
3143
|
+
return setApiResponse6(HTTP6.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
|
|
3050
3144
|
}
|
|
3051
3145
|
const fullUser = await users.findById(upserted.id);
|
|
3052
3146
|
if (!fullUser) {
|
|
3053
|
-
return
|
|
3147
|
+
return setApiResponse6(HTTP6.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
|
|
3054
3148
|
}
|
|
3055
3149
|
await announceOAuthUpsert(ctx, upserted, provider, fullUser);
|
|
3056
3150
|
if (fullUser.suspended) {
|
|
3057
|
-
return
|
|
3151
|
+
return setApiResponse6(HTTP6.FORBIDDEN, "ACCOUNT_SUSPENDED", "Account suspended. Please contact support.");
|
|
3058
3152
|
}
|
|
3059
3153
|
if (fullUser.mfaEnabled) {
|
|
3060
3154
|
const mfaToken = issueMfaPendingToken(upserted.id, config, provider);
|
|
@@ -3097,8 +3191,8 @@ function oauthController(store, config, bus) {
|
|
|
3097
3191
|
googleInit: async (_ctx) => {
|
|
3098
3192
|
const google = config.google;
|
|
3099
3193
|
if (!google || !google.clientSecret || !google.redirectUri) {
|
|
3100
|
-
return
|
|
3101
|
-
|
|
3194
|
+
return setApiResponse6(
|
|
3195
|
+
HTTP6.NOT_IMPLEMENTED,
|
|
3102
3196
|
"NOT_CONFIGURED",
|
|
3103
3197
|
"Google OAuth not configured"
|
|
3104
3198
|
);
|
|
@@ -3132,8 +3226,8 @@ function oauthController(store, config, bus) {
|
|
|
3132
3226
|
googleCallback: async (ctx) => {
|
|
3133
3227
|
const google = config.google;
|
|
3134
3228
|
if (!google || !google.clientSecret || !google.redirectUri) {
|
|
3135
|
-
return
|
|
3136
|
-
|
|
3229
|
+
return setApiResponse6(
|
|
3230
|
+
HTTP6.NOT_IMPLEMENTED,
|
|
3137
3231
|
"NOT_CONFIGURED",
|
|
3138
3232
|
"Google OAuth not configured"
|
|
3139
3233
|
);
|
|
@@ -3141,14 +3235,14 @@ function oauthController(store, config, bus) {
|
|
|
3141
3235
|
const url = new URL(ctx.request.url);
|
|
3142
3236
|
const code = url.searchParams.get("code");
|
|
3143
3237
|
if (!code) {
|
|
3144
|
-
return
|
|
3238
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
3145
3239
|
}
|
|
3146
3240
|
const returnedState = url.searchParams.get("state") ?? "";
|
|
3147
3241
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
3148
3242
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
3149
3243
|
if (!returnedState || !expectedState || !constantTimeEqual2(Buffer.from(returnedState), Buffer.from(expectedState))) {
|
|
3150
|
-
return
|
|
3151
|
-
|
|
3244
|
+
return setApiResponse6(
|
|
3245
|
+
HTTP6.BAD_REQUEST,
|
|
3152
3246
|
"GOOGLE_AUTH_FAILED",
|
|
3153
3247
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
3154
3248
|
);
|
|
@@ -3166,8 +3260,8 @@ function oauthController(store, config, bus) {
|
|
|
3166
3260
|
});
|
|
3167
3261
|
const tokenData = await tokenRes.json();
|
|
3168
3262
|
if (!tokenData.id_token) {
|
|
3169
|
-
return
|
|
3170
|
-
|
|
3263
|
+
return setApiResponse6(
|
|
3264
|
+
HTTP6.BAD_REQUEST,
|
|
3171
3265
|
"GOOGLE_AUTH_FAILED",
|
|
3172
3266
|
"OAuth token exchange failed"
|
|
3173
3267
|
);
|
|
@@ -3176,20 +3270,20 @@ function oauthController(store, config, bus) {
|
|
|
3176
3270
|
Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
|
|
3177
3271
|
);
|
|
3178
3272
|
if (payload.aud !== google.clientId) {
|
|
3179
|
-
return
|
|
3273
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
|
|
3180
3274
|
}
|
|
3181
3275
|
if (payload.iss !== "https://accounts.google.com" && payload.iss !== "accounts.google.com") {
|
|
3182
|
-
return
|
|
3276
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
|
|
3183
3277
|
}
|
|
3184
3278
|
if (typeof payload.exp !== "number" || payload.exp * 1e3 < Date.now()) {
|
|
3185
|
-
return
|
|
3279
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
|
|
3186
3280
|
}
|
|
3187
3281
|
if (!payload.email) {
|
|
3188
|
-
return
|
|
3282
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
|
|
3189
3283
|
}
|
|
3190
3284
|
if (payload.email_verified !== true) {
|
|
3191
|
-
return
|
|
3192
|
-
|
|
3285
|
+
return setApiResponse6(
|
|
3286
|
+
HTTP6.BAD_REQUEST,
|
|
3193
3287
|
"GOOGLE_AUTH_FAILED",
|
|
3194
3288
|
"Google account email is not verified"
|
|
3195
3289
|
);
|
|
@@ -3203,7 +3297,7 @@ function oauthController(store, config, bus) {
|
|
|
3203
3297
|
appleInit: async (_ctx) => {
|
|
3204
3298
|
const apple = config.apple;
|
|
3205
3299
|
if (!apple) {
|
|
3206
|
-
return
|
|
3300
|
+
return setApiResponse6(HTTP6.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3207
3301
|
}
|
|
3208
3302
|
const state = randomBytes4(16).toString("hex");
|
|
3209
3303
|
const params = new URLSearchParams({
|
|
@@ -3234,20 +3328,20 @@ function oauthController(store, config, bus) {
|
|
|
3234
3328
|
appleCallback: async (ctx) => {
|
|
3235
3329
|
const apple = config.apple;
|
|
3236
3330
|
if (!apple) {
|
|
3237
|
-
return
|
|
3331
|
+
return setApiResponse6(HTTP6.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3238
3332
|
}
|
|
3239
3333
|
const pre = ctx.meta["body"];
|
|
3240
3334
|
const form = pre && typeof pre === "object" ? new URLSearchParams(pre) : new URLSearchParams(await ctx.request.text());
|
|
3241
3335
|
const code = form.get("code");
|
|
3242
3336
|
if (!code) {
|
|
3243
|
-
return
|
|
3337
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
3244
3338
|
}
|
|
3245
3339
|
const returnedState = form.get("state") ?? "";
|
|
3246
3340
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
3247
3341
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
3248
3342
|
if (!returnedState || !expectedState || !constantTimeEqual2(Buffer.from(returnedState), Buffer.from(expectedState))) {
|
|
3249
|
-
return
|
|
3250
|
-
|
|
3343
|
+
return setApiResponse6(
|
|
3344
|
+
HTTP6.BAD_REQUEST,
|
|
3251
3345
|
"APPLE_AUTH_FAILED",
|
|
3252
3346
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
3253
3347
|
);
|
|
@@ -3265,11 +3359,11 @@ function oauthController(store, config, bus) {
|
|
|
3265
3359
|
});
|
|
3266
3360
|
const tokenData = await tokenRes.json();
|
|
3267
3361
|
if (!tokenData.id_token) {
|
|
3268
|
-
return
|
|
3362
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
|
|
3269
3363
|
}
|
|
3270
3364
|
const claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });
|
|
3271
3365
|
if (!claims) {
|
|
3272
|
-
return
|
|
3366
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3273
3367
|
}
|
|
3274
3368
|
return completeOAuthLogin(ctx, "apple", {
|
|
3275
3369
|
email: claims.email,
|
|
@@ -3280,12 +3374,12 @@ function oauthController(store, config, bus) {
|
|
|
3280
3374
|
appleNative: async (ctx) => {
|
|
3281
3375
|
const apple = config.apple;
|
|
3282
3376
|
if (!apple) {
|
|
3283
|
-
return
|
|
3377
|
+
return setApiResponse6(HTTP6.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3284
3378
|
}
|
|
3285
3379
|
const body = ctx.meta["body"];
|
|
3286
3380
|
const identityToken = body?.identityToken;
|
|
3287
3381
|
if (!identityToken) {
|
|
3288
|
-
return
|
|
3382
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
|
|
3289
3383
|
}
|
|
3290
3384
|
const audiences = apple.nativeClientIds && apple.nativeClientIds.length ? apple.nativeClientIds : [apple.clientId];
|
|
3291
3385
|
const claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });
|
|
@@ -3299,7 +3393,7 @@ function oauthController(store, config, bus) {
|
|
|
3299
3393
|
failureReason: "invalid_identity_token",
|
|
3300
3394
|
...meta
|
|
3301
3395
|
});
|
|
3302
|
-
return
|
|
3396
|
+
return setApiResponse6(HTTP6.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3303
3397
|
}
|
|
3304
3398
|
const tokenHash = createHash4("sha256").update(identityToken).digest("hex");
|
|
3305
3399
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
@@ -3314,8 +3408,8 @@ function oauthController(store, config, bus) {
|
|
|
3314
3408
|
failureReason: "token_replayed",
|
|
3315
3409
|
...meta
|
|
3316
3410
|
});
|
|
3317
|
-
return
|
|
3318
|
-
|
|
3411
|
+
return setApiResponse6(
|
|
3412
|
+
HTTP6.UNAUTHORIZED,
|
|
3319
3413
|
"APPLE_AUTH_FAILED",
|
|
3320
3414
|
"This Apple identity token has already been used"
|
|
3321
3415
|
);
|
|
@@ -3332,12 +3426,12 @@ function oauthController(store, config, bus) {
|
|
|
3332
3426
|
googleNative: async (ctx) => {
|
|
3333
3427
|
const audiences = config.google?.nativeClientIds ?? [];
|
|
3334
3428
|
if (!config.google || audiences.length === 0) {
|
|
3335
|
-
return
|
|
3429
|
+
return setApiResponse6(HTTP6.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Native Google sign-in not configured");
|
|
3336
3430
|
}
|
|
3337
3431
|
const body = ctx.meta["body"];
|
|
3338
3432
|
const idToken = body?.idToken;
|
|
3339
3433
|
if (!idToken) {
|
|
3340
|
-
return
|
|
3434
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "INVALID_PARAMETER", "Missing idToken");
|
|
3341
3435
|
}
|
|
3342
3436
|
const claims = await verifyGoogleIdToken(idToken, { audiences, nonce: body?.nonce });
|
|
3343
3437
|
if (!claims) {
|
|
@@ -3349,7 +3443,7 @@ function oauthController(store, config, bus) {
|
|
|
3349
3443
|
failureReason: "invalid_identity_token",
|
|
3350
3444
|
...requestMeta(ctx)
|
|
3351
3445
|
});
|
|
3352
|
-
return
|
|
3446
|
+
return setApiResponse6(HTTP6.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "Invalid Google ID token");
|
|
3353
3447
|
}
|
|
3354
3448
|
const tokenHash = createHash4("sha256").update(idToken).digest("hex");
|
|
3355
3449
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
@@ -3362,7 +3456,7 @@ function oauthController(store, config, bus) {
|
|
|
3362
3456
|
failureReason: "token_replayed",
|
|
3363
3457
|
...requestMeta(ctx)
|
|
3364
3458
|
});
|
|
3365
|
-
return
|
|
3459
|
+
return setApiResponse6(HTTP6.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "This Google ID token has already been used");
|
|
3366
3460
|
}
|
|
3367
3461
|
consumedTokens.sweepExpiredSafe();
|
|
3368
3462
|
return completeOAuthLogin(ctx, "google", {
|
|
@@ -3408,7 +3502,7 @@ function buildAuthRoutes(store, config, bus, locales) {
|
|
|
3408
3502
|
"providers",
|
|
3409
3503
|
"GET",
|
|
3410
3504
|
"/auth/providers",
|
|
3411
|
-
async () =>
|
|
3505
|
+
async () => setApiResponse7(HTTP7.OK, "AUTH_PROVIDERS", "Sign-in methods available here", {
|
|
3412
3506
|
providers: [...config.providers]
|
|
3413
3507
|
})
|
|
3414
3508
|
),
|
|
@@ -3672,7 +3766,7 @@ function extractToken(request) {
|
|
|
3672
3766
|
}
|
|
3673
3767
|
|
|
3674
3768
|
// src/admin.ts
|
|
3675
|
-
import { HTTP as
|
|
3769
|
+
import { HTTP as HTTP8, encodeKeysetCursor as encodeKeysetCursor2, setApiResponse as setApiResponse8 } from "@fonderie/core";
|
|
3676
3770
|
import { background as background6 } from "@fonderie/core";
|
|
3677
3771
|
function toAdminUserDTO(user) {
|
|
3678
3772
|
return {
|
|
@@ -3689,7 +3783,7 @@ function toAdminUserPageDTO(page) {
|
|
|
3689
3783
|
}
|
|
3690
3784
|
var ADMIN_PREFIX = "/_admin";
|
|
3691
3785
|
var idOf = (ctx) => ctx.meta.params?.["id"] ?? "";
|
|
3692
|
-
var NOT_FOUND = () =>
|
|
3786
|
+
var NOT_FOUND = () => setApiResponse8(HTTP8.NOT_FOUND, "NOT_FOUND", "No such user");
|
|
3693
3787
|
function adminRouteTable(store, bus) {
|
|
3694
3788
|
const users = new UserModel(store);
|
|
3695
3789
|
const sessions = new SessionModel(store);
|
|
@@ -3698,8 +3792,8 @@ function adminRouteTable(store, bus) {
|
|
|
3698
3792
|
const ok = await users.setSuspended(idOf(ctx), suspended);
|
|
3699
3793
|
if (!ok) return NOT_FOUND();
|
|
3700
3794
|
const user = await users.findById(idOf(ctx));
|
|
3701
|
-
return user ?
|
|
3702
|
-
|
|
3795
|
+
return user ? setApiResponse8(
|
|
3796
|
+
HTTP8.OK,
|
|
3703
3797
|
suspended ? "USER_SUSPENDED" : "USER_UNSUSPENDED",
|
|
3704
3798
|
suspended ? "User suspended" : "User unsuspended",
|
|
3705
3799
|
toAdminUserDTO(user)
|
|
@@ -3718,21 +3812,21 @@ function adminRouteTable(store, bus) {
|
|
|
3718
3812
|
const email2 = raw ? normalizeEmailSafe(raw) ?? raw.toLowerCase() : void 0;
|
|
3719
3813
|
if (email2) {
|
|
3720
3814
|
const user = await users.findByEmail(email2);
|
|
3721
|
-
return user ?
|
|
3815
|
+
return user ? setApiResponse8(HTTP8.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
|
|
3722
3816
|
}
|
|
3723
3817
|
const rawLimit = Number(params.get("limit") ?? 50);
|
|
3724
3818
|
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
3725
3819
|
const cursorParam = params.get("cursor");
|
|
3726
3820
|
const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
|
|
3727
3821
|
if (cursorParam && !cursor)
|
|
3728
|
-
return
|
|
3822
|
+
return setApiResponse8(HTTP8.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
3729
3823
|
const deleted = params.get("deleted") === "1" || params.get("deleted") === "true";
|
|
3730
3824
|
const page = await users.list({
|
|
3731
3825
|
limit,
|
|
3732
3826
|
...cursor ? { cursor } : {},
|
|
3733
3827
|
...deleted ? { deleted } : {}
|
|
3734
3828
|
});
|
|
3735
|
-
return
|
|
3829
|
+
return setApiResponse8(HTTP8.OK, "USERS", "Users", toAdminUserPageDTO(page));
|
|
3736
3830
|
}
|
|
3737
3831
|
],
|
|
3738
3832
|
[
|
|
@@ -3740,7 +3834,7 @@ function adminRouteTable(store, bus) {
|
|
|
3740
3834
|
"/_admin/users/:id",
|
|
3741
3835
|
async (ctx) => {
|
|
3742
3836
|
const user = await users.findByIdIncludingDeleted(idOf(ctx));
|
|
3743
|
-
return user ?
|
|
3837
|
+
return user ? setApiResponse8(HTTP8.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
|
|
3744
3838
|
}
|
|
3745
3839
|
],
|
|
3746
3840
|
[
|
|
@@ -3749,8 +3843,8 @@ function adminRouteTable(store, bus) {
|
|
|
3749
3843
|
async (ctx) => {
|
|
3750
3844
|
if (!await users.findById(idOf(ctx))) return NOT_FOUND();
|
|
3751
3845
|
const rows = await sessions.listLiveByUser(idOf(ctx));
|
|
3752
|
-
return
|
|
3753
|
-
|
|
3846
|
+
return setApiResponse8(
|
|
3847
|
+
HTTP8.OK,
|
|
3754
3848
|
"SESSIONS",
|
|
3755
3849
|
"Live sessions",
|
|
3756
3850
|
rows.map((r) => toSessionDTO(r, null))
|
|
@@ -3765,7 +3859,7 @@ function adminRouteTable(store, bus) {
|
|
|
3765
3859
|
if (!await users.findById(idOf(ctx))) return NOT_FOUND();
|
|
3766
3860
|
await sessions.deleteByUser(idOf(ctx));
|
|
3767
3861
|
await background6(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: idOf(ctx), sids: null, reason: "admin" }));
|
|
3768
|
-
return
|
|
3862
|
+
return setApiResponse8(HTTP8.OK, "SESSIONS_REVOKED", "All sessions revoked");
|
|
3769
3863
|
}
|
|
3770
3864
|
],
|
|
3771
3865
|
[
|
|
@@ -3779,14 +3873,14 @@ function adminRouteTable(store, bus) {
|
|
|
3779
3873
|
const cursorParam = params.get("cursor");
|
|
3780
3874
|
const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
|
|
3781
3875
|
if (cursorParam && !cursor)
|
|
3782
|
-
return
|
|
3876
|
+
return setApiResponse8(HTTP8.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
3783
3877
|
const page = await events.listByUser({
|
|
3784
3878
|
userId: idOf(ctx),
|
|
3785
3879
|
limit,
|
|
3786
3880
|
...cursor ? { cursor } : {}
|
|
3787
3881
|
});
|
|
3788
|
-
return
|
|
3789
|
-
|
|
3882
|
+
return setApiResponse8(
|
|
3883
|
+
HTTP8.OK,
|
|
3790
3884
|
"LOGIN_HISTORY",
|
|
3791
3885
|
"Login history",
|
|
3792
3886
|
toLoginHistoryPageDTO(page)
|
|
@@ -3819,7 +3913,7 @@ var AuthModule = class {
|
|
|
3819
3913
|
name = "@fonderie/auth";
|
|
3820
3914
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3821
3915
|
// say what is actually deployed rather than 'not reported'.
|
|
3822
|
-
version = "7.
|
|
3916
|
+
version = "7.23.0";
|
|
3823
3917
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3824
3918
|
describeAdmin() {
|
|
3825
3919
|
return { routes: describeAuthAdminRoutes(this.store, this.bus) };
|