@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.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
|
|
@@ -389,6 +389,10 @@ var EVENT_KEYS = {
|
|
|
389
389
|
// 'password-changed', 'admin', 'refresh-reuse' (theft signal).
|
|
390
390
|
sessionRevoked: "fonderie.session.revoked"
|
|
391
391
|
};
|
|
392
|
+
var deletionGracePeriodDays = (config) => {
|
|
393
|
+
const days = config.accountDeletion?.gracePeriodDays;
|
|
394
|
+
return typeof days === "number" && Number.isFinite(days) && days >= 0 ? days : 30;
|
|
395
|
+
};
|
|
392
396
|
|
|
393
397
|
// src/services/jwt.ts
|
|
394
398
|
var import_node_crypto = require("crypto");
|
|
@@ -805,6 +809,27 @@ var UserModel = class {
|
|
|
805
809
|
);
|
|
806
810
|
return row ?? null;
|
|
807
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
|
+
}
|
|
808
833
|
async findByPhone(phone2) {
|
|
809
834
|
const [row] = await this.store.query(
|
|
810
835
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
|
|
@@ -1659,7 +1684,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1659
1684
|
// src/controllers/auth.controller.ts
|
|
1660
1685
|
var import_node_crypto6 = require("crypto");
|
|
1661
1686
|
var import_events2 = require("@fonderie/events");
|
|
1662
|
-
var
|
|
1687
|
+
var import_core7 = require("@fonderie/core");
|
|
1663
1688
|
|
|
1664
1689
|
// src/services/cooldown.ts
|
|
1665
1690
|
function checkCooldown(lastSentAt, cooldownMs) {
|
|
@@ -1670,6 +1695,28 @@ function checkCooldown(lastSentAt, cooldownMs) {
|
|
|
1670
1695
|
// src/controllers/auth.controller.ts
|
|
1671
1696
|
init_password();
|
|
1672
1697
|
|
|
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);
|
|
1702
|
+
}
|
|
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
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1673
1720
|
// src/models/password-reset.model.ts
|
|
1674
1721
|
var import_node_crypto5 = require("crypto");
|
|
1675
1722
|
var hashSecret = (value) => (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
|
|
@@ -1826,7 +1873,7 @@ function extractRefreshToken(ctx) {
|
|
|
1826
1873
|
const match = cookie.match(/(?:^|;\s*)refresh_token=([^;]+)/);
|
|
1827
1874
|
return match?.[1] ?? null;
|
|
1828
1875
|
}
|
|
1829
|
-
function authController(store, config, bus, locales = (0,
|
|
1876
|
+
function authController(store, config, bus, locales = (0, import_core7.defineLocales)()) {
|
|
1830
1877
|
const users = new UserModel(store);
|
|
1831
1878
|
const sessions = new SessionModel(store, config.location);
|
|
1832
1879
|
const loginEvents = new LoginEventModel(store, config.location);
|
|
@@ -1838,23 +1885,24 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1838
1885
|
register: async (ctx) => {
|
|
1839
1886
|
const body = ctx.meta["body"];
|
|
1840
1887
|
const { email: email2, password: password2, phone: phone2, firstName = null, lastName = null } = body ?? {};
|
|
1841
|
-
const locale = (0,
|
|
1888
|
+
const locale = (0, import_core7.canonicalLocale)(body?.["locale"]) ?? locales.default;
|
|
1842
1889
|
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
1843
1890
|
const normalizedEmail = normalizeEmailSafe(email2);
|
|
1844
1891
|
if (!normalizedEmail) {
|
|
1845
|
-
return (0,
|
|
1892
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1846
1893
|
}
|
|
1847
1894
|
if (password2.length < 8) {
|
|
1848
|
-
return (0,
|
|
1849
|
-
|
|
1895
|
+
return (0, import_core7.setApiResponse)(
|
|
1896
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
1850
1897
|
"INVALID_PARAMETER",
|
|
1851
1898
|
"password must be at least 8 characters"
|
|
1852
1899
|
);
|
|
1853
1900
|
}
|
|
1854
1901
|
const existing = await users.findByEmail(normalizedEmail);
|
|
1855
1902
|
if (existing) {
|
|
1856
|
-
return (0,
|
|
1903
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
1857
1904
|
}
|
|
1905
|
+
if (await users.findArchivedByEmail(normalizedEmail)) return archivedAddressResponse();
|
|
1858
1906
|
const passwordHash = await hashPassword(password2);
|
|
1859
1907
|
const row = await users.create(
|
|
1860
1908
|
normalizedEmail,
|
|
@@ -1864,18 +1912,18 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1864
1912
|
locale
|
|
1865
1913
|
);
|
|
1866
1914
|
if (!row) {
|
|
1867
|
-
return (0,
|
|
1915
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1868
1916
|
}
|
|
1869
1917
|
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
1870
1918
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1871
1919
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1872
1920
|
const user = await users.findById(row.id);
|
|
1873
1921
|
if (!user) {
|
|
1874
|
-
return (0,
|
|
1922
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1875
1923
|
}
|
|
1876
1924
|
const reqId = ctx.meta["requestId"];
|
|
1877
1925
|
const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
|
|
1878
|
-
await (0,
|
|
1926
|
+
await (0, import_core7.background)(bus?.emit(
|
|
1879
1927
|
import_events2.NOTIFICATION_EVENT,
|
|
1880
1928
|
{
|
|
1881
1929
|
type: MESSAGE_KEYS.emailRegistration,
|
|
@@ -1885,7 +1933,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1885
1933
|
},
|
|
1886
1934
|
reqOpts
|
|
1887
1935
|
));
|
|
1888
|
-
await (0,
|
|
1936
|
+
await (0, import_core7.background)(bus?.emit(
|
|
1889
1937
|
EVENT_KEYS.userRegistered,
|
|
1890
1938
|
{
|
|
1891
1939
|
userId: user.id,
|
|
@@ -1929,8 +1977,9 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1929
1977
|
if (isValidPhone(phone2)) {
|
|
1930
1978
|
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
1931
1979
|
if (existing) {
|
|
1932
|
-
return (0,
|
|
1980
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
1933
1981
|
}
|
|
1982
|
+
if (await users.findArchivedByPhone(normalizePhone(phone2))) return archivedAddressResponse();
|
|
1934
1983
|
const { id } = await users.findOrCreateByPhone(
|
|
1935
1984
|
normalizePhone(phone2),
|
|
1936
1985
|
firstName ?? null,
|
|
@@ -1942,11 +1991,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1942
1991
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1943
1992
|
const user = await users.findById(id);
|
|
1944
1993
|
if (!user) {
|
|
1945
|
-
return (0,
|
|
1994
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1946
1995
|
}
|
|
1947
1996
|
const reqId2 = ctx.meta["requestId"];
|
|
1948
1997
|
const reqOpts2 = reqId2 !== void 0 ? { requestId: reqId2 } : void 0;
|
|
1949
|
-
await (0,
|
|
1998
|
+
await (0, import_core7.background)(bus?.emit(
|
|
1950
1999
|
import_events2.NOTIFICATION_EVENT,
|
|
1951
2000
|
{
|
|
1952
2001
|
type: MESSAGE_KEYS.phoneOtp,
|
|
@@ -1956,7 +2005,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1956
2005
|
},
|
|
1957
2006
|
reqOpts2
|
|
1958
2007
|
));
|
|
1959
|
-
await (0,
|
|
2008
|
+
await (0, import_core7.background)(bus?.emit(
|
|
1960
2009
|
EVENT_KEYS.userRegistered,
|
|
1961
2010
|
{
|
|
1962
2011
|
userId: user.id,
|
|
@@ -1980,8 +2029,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1980
2029
|
{ status: 202 }
|
|
1981
2030
|
);
|
|
1982
2031
|
}
|
|
1983
|
-
return (0,
|
|
1984
|
-
|
|
2032
|
+
return (0, import_core7.setApiResponse)(
|
|
2033
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
1985
2034
|
"INVALID_PARAMETER",
|
|
1986
2035
|
"Provide email + password or a valid phone number"
|
|
1987
2036
|
);
|
|
@@ -1993,9 +2042,25 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1993
2042
|
const { email: rawEmail, password: password2 } = body;
|
|
1994
2043
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
1995
2044
|
if (!email2) {
|
|
1996
|
-
return (0,
|
|
2045
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1997
2046
|
}
|
|
1998
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
|
+
}
|
|
1999
2064
|
if (!user || !user.passwordHash) {
|
|
2000
2065
|
await loginEvents.recordSafe({
|
|
2001
2066
|
userId: null,
|
|
@@ -2005,7 +2070,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2005
2070
|
failureReason: "unknown_email",
|
|
2006
2071
|
...meta
|
|
2007
2072
|
});
|
|
2008
|
-
return (0,
|
|
2073
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2009
2074
|
}
|
|
2010
2075
|
const { valid, needsRehash } = await verifyPasswordForLogin(
|
|
2011
2076
|
password2,
|
|
@@ -2021,21 +2086,21 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2021
2086
|
failureReason: "bad_password",
|
|
2022
2087
|
...meta
|
|
2023
2088
|
});
|
|
2024
|
-
return (0,
|
|
2089
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2025
2090
|
}
|
|
2026
2091
|
if (needsRehash) {
|
|
2027
2092
|
await users.updatePassword(user.id, await hashPassword(password2));
|
|
2028
2093
|
}
|
|
2029
2094
|
if (user.suspended) {
|
|
2030
|
-
return (0,
|
|
2031
|
-
|
|
2095
|
+
return (0, import_core7.setApiResponse)(
|
|
2096
|
+
import_core7.HTTP.FORBIDDEN,
|
|
2032
2097
|
"ACCOUNT_SUSPENDED",
|
|
2033
2098
|
"Account suspended. Please contact support."
|
|
2034
2099
|
);
|
|
2035
2100
|
}
|
|
2036
2101
|
if (user.mfaEnabled) {
|
|
2037
2102
|
const mfaToken = issueMfaPendingToken(user.id, config, "email");
|
|
2038
|
-
return (0,
|
|
2103
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
|
|
2039
2104
|
mfaToken
|
|
2040
2105
|
});
|
|
2041
2106
|
}
|
|
@@ -2072,11 +2137,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2072
2137
|
if (isValidPhone(phone2)) {
|
|
2073
2138
|
const user = await users.findByPhone(normalizePhone(phone2));
|
|
2074
2139
|
if (!user) {
|
|
2075
|
-
return (0,
|
|
2140
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
2076
2141
|
}
|
|
2077
2142
|
if (user.suspended) {
|
|
2078
|
-
return (0,
|
|
2079
|
-
|
|
2143
|
+
return (0, import_core7.setApiResponse)(
|
|
2144
|
+
import_core7.HTTP.FORBIDDEN,
|
|
2080
2145
|
"ACCOUNT_SUSPENDED",
|
|
2081
2146
|
"Account suspended. Please contact support."
|
|
2082
2147
|
);
|
|
@@ -2084,7 +2149,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2084
2149
|
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2085
2150
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
2086
2151
|
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
2087
|
-
await (0,
|
|
2152
|
+
await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
2088
2153
|
type: MESSAGE_KEYS.phoneOtp,
|
|
2089
2154
|
locale: user.locale,
|
|
2090
2155
|
data: { otp },
|
|
@@ -2100,8 +2165,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2100
2165
|
{ status: 202 }
|
|
2101
2166
|
);
|
|
2102
2167
|
}
|
|
2103
|
-
return (0,
|
|
2104
|
-
|
|
2168
|
+
return (0, import_core7.setApiResponse)(
|
|
2169
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
2105
2170
|
"INVALID_PARAMETER",
|
|
2106
2171
|
"Provide email + password or a valid phone number"
|
|
2107
2172
|
);
|
|
@@ -2126,35 +2191,35 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2126
2191
|
refresh: async (ctx) => {
|
|
2127
2192
|
const token = extractRefreshToken(ctx);
|
|
2128
2193
|
if (!token) {
|
|
2129
|
-
return (0,
|
|
2194
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
|
|
2130
2195
|
}
|
|
2131
2196
|
const payload = verifyToken(token, config);
|
|
2132
2197
|
if (!payload || payload.type !== "refresh") {
|
|
2133
|
-
return (0,
|
|
2198
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
2134
2199
|
}
|
|
2135
2200
|
const runtime = config.resolve?.(ctx);
|
|
2136
2201
|
let issued = null;
|
|
2137
2202
|
for (let attempt = 0; attempt < 3 && !issued; attempt++) {
|
|
2138
2203
|
const found = await sessions.match(token);
|
|
2139
2204
|
if (found.kind === "unknown") {
|
|
2140
|
-
return (0,
|
|
2205
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2141
2206
|
}
|
|
2142
2207
|
if (found.kind === "reused") {
|
|
2143
2208
|
await sessions.revokeById(found.row.id);
|
|
2144
|
-
await (0,
|
|
2209
|
+
await (0, import_core7.background)(
|
|
2145
2210
|
bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sids: [found.row.sid], reason: "refresh-reuse" })
|
|
2146
2211
|
);
|
|
2147
|
-
return (0,
|
|
2212
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2148
2213
|
}
|
|
2149
2214
|
const resolvedRefresh = configForClient(config, runtime, found.row.clientKind);
|
|
2150
2215
|
const maxAgeMs = resolvedRefresh.sessionMaxAge ? durationMs(String(resolvedRefresh.sessionMaxAge)) : null;
|
|
2151
2216
|
if (maxAgeMs !== null && Date.now() - found.row.createdAt.getTime() > maxAgeMs) {
|
|
2152
2217
|
await sessions.revokeById(found.row.id);
|
|
2153
|
-
return (0,
|
|
2218
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
|
|
2154
2219
|
}
|
|
2155
2220
|
const user = await users.findById(found.row.userId);
|
|
2156
2221
|
if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
|
|
2157
|
-
return (0,
|
|
2222
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
2158
2223
|
}
|
|
2159
2224
|
const pair = issueTokenPair(user.id, resolvedRefresh, {
|
|
2160
2225
|
loginMethod: payload.loginMethod ?? "email",
|
|
@@ -2167,7 +2232,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2167
2232
|
if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
|
|
2168
2233
|
}
|
|
2169
2234
|
if (!issued) {
|
|
2170
|
-
return (0,
|
|
2235
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2171
2236
|
}
|
|
2172
2237
|
const { accessToken, refreshToken } = issued;
|
|
2173
2238
|
return Response.json(
|
|
@@ -2186,16 +2251,16 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2186
2251
|
const body = ctx.meta["body"];
|
|
2187
2252
|
const rawEmail = body?.["email"];
|
|
2188
2253
|
if (typeof rawEmail !== "string") {
|
|
2189
|
-
return (0,
|
|
2254
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
2190
2255
|
}
|
|
2191
2256
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
2192
2257
|
if (!email2) {
|
|
2193
|
-
return (0,
|
|
2258
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
2194
2259
|
}
|
|
2195
2260
|
const user = await users.findByEmail(email2);
|
|
2196
2261
|
if (!user) {
|
|
2197
|
-
return (0,
|
|
2198
|
-
|
|
2262
|
+
return (0, import_core7.setApiResponse)(
|
|
2263
|
+
import_core7.HTTP.OK,
|
|
2199
2264
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
2200
2265
|
"Password reset email sent (if account exists)."
|
|
2201
2266
|
);
|
|
@@ -2204,8 +2269,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2204
2269
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
2205
2270
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
2206
2271
|
if (remaining > 0) {
|
|
2207
|
-
return (0,
|
|
2208
|
-
|
|
2272
|
+
return (0, import_core7.setApiResponse)(
|
|
2273
|
+
import_core7.HTTP.OK,
|
|
2209
2274
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
2210
2275
|
"Password reset email sent (if account exists)."
|
|
2211
2276
|
);
|
|
@@ -2216,14 +2281,14 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2216
2281
|
await passwordReset.create(user.id, pin, token, expiresAt);
|
|
2217
2282
|
const base = resolved.passwordResetUrl;
|
|
2218
2283
|
const resetUrl = base ? `${base}${base.includes("?") ? "&" : "?"}token=${token}` : "";
|
|
2219
|
-
await (0,
|
|
2284
|
+
await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
2220
2285
|
type: MESSAGE_KEYS.passwordReset,
|
|
2221
2286
|
locale: user.locale,
|
|
2222
2287
|
recipient: { email: email2, phone: null, deviceToken: null },
|
|
2223
2288
|
data: { pin, token, resetUrl }
|
|
2224
2289
|
}));
|
|
2225
|
-
return (0,
|
|
2226
|
-
|
|
2290
|
+
return (0, import_core7.setApiResponse)(
|
|
2291
|
+
import_core7.HTTP.OK,
|
|
2227
2292
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
2228
2293
|
"Password reset email sent (if account exists)."
|
|
2229
2294
|
);
|
|
@@ -2234,11 +2299,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2234
2299
|
const password2 = body?.["password"];
|
|
2235
2300
|
const rawToken = body?.["token"];
|
|
2236
2301
|
if (typeof password2 !== "string") {
|
|
2237
|
-
return (0,
|
|
2302
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
|
|
2238
2303
|
}
|
|
2239
2304
|
if (password2.length < 8) {
|
|
2240
|
-
return (0,
|
|
2241
|
-
|
|
2305
|
+
return (0, import_core7.setApiResponse)(
|
|
2306
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
2242
2307
|
"INVALID_PARAMETER",
|
|
2243
2308
|
"password must be at least 8 characters"
|
|
2244
2309
|
);
|
|
@@ -2248,8 +2313,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2248
2313
|
row = await passwordReset.findByToken(rawToken.trim());
|
|
2249
2314
|
} else {
|
|
2250
2315
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
2251
|
-
return (0,
|
|
2252
|
-
|
|
2316
|
+
return (0, import_core7.setApiResponse)(
|
|
2317
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
2253
2318
|
"INVALID_PARAMETER",
|
|
2254
2319
|
"a 6-digit pin or a reset token is required"
|
|
2255
2320
|
);
|
|
@@ -2257,33 +2322,41 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2257
2322
|
row = await passwordReset.findByPin(raw.trim());
|
|
2258
2323
|
}
|
|
2259
2324
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
2260
|
-
return (0,
|
|
2261
|
-
|
|
2325
|
+
return (0, import_core7.setApiResponse)(
|
|
2326
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2262
2327
|
"PASSWORD_RESET_FAILED",
|
|
2263
2328
|
"Invalid or expired reset credentials"
|
|
2264
2329
|
);
|
|
2265
2330
|
}
|
|
2266
2331
|
const passwordHash = await hashPassword(password2);
|
|
2267
|
-
await store.
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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.");
|
|
2280
2353
|
},
|
|
2281
2354
|
verify: async (ctx) => {
|
|
2282
2355
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
2283
|
-
return (0,
|
|
2356
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
2284
2357
|
}
|
|
2285
2358
|
if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
|
|
2286
|
-
return (0,
|
|
2359
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
2287
2360
|
verified: true,
|
|
2288
2361
|
email: ctx.user.email
|
|
2289
2362
|
});
|
|
@@ -2291,8 +2364,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2291
2364
|
const body = ctx.meta["body"];
|
|
2292
2365
|
const raw = body?.["token"];
|
|
2293
2366
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
2294
|
-
return (0,
|
|
2295
|
-
|
|
2367
|
+
return (0, import_core7.setApiResponse)(
|
|
2368
|
+
import_core7.HTTP.UNPROCESSABLE,
|
|
2296
2369
|
"INVALID_PARAMETER",
|
|
2297
2370
|
"token must be a 6-digit code"
|
|
2298
2371
|
);
|
|
@@ -2310,7 +2383,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2310
2383
|
failureReason: "invalid_pin",
|
|
2311
2384
|
...phoneMeta
|
|
2312
2385
|
});
|
|
2313
|
-
return (0,
|
|
2386
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
2314
2387
|
}
|
|
2315
2388
|
if (/* @__PURE__ */ new Date() > record.expiresAt) {
|
|
2316
2389
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
@@ -2322,8 +2395,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2322
2395
|
failureReason: "expired_pin",
|
|
2323
2396
|
...phoneMeta
|
|
2324
2397
|
});
|
|
2325
|
-
return (0,
|
|
2326
|
-
|
|
2398
|
+
return (0, import_core7.setApiResponse)(
|
|
2399
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2327
2400
|
"VERIFICATION_FAILED",
|
|
2328
2401
|
"Verification code expired"
|
|
2329
2402
|
);
|
|
@@ -2343,11 +2416,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2343
2416
|
});
|
|
2344
2417
|
const verifiedUser = await users.findById(ctx.user.id);
|
|
2345
2418
|
if (!verifiedUser) {
|
|
2346
|
-
return (0,
|
|
2419
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2347
2420
|
}
|
|
2348
2421
|
if (verifiedUser.suspended) {
|
|
2349
|
-
return (0,
|
|
2350
|
-
|
|
2422
|
+
return (0, import_core7.setApiResponse)(
|
|
2423
|
+
import_core7.HTTP.FORBIDDEN,
|
|
2351
2424
|
"ACCOUNT_SUSPENDED",
|
|
2352
2425
|
"Account suspended. Please contact support."
|
|
2353
2426
|
);
|
|
@@ -2369,10 +2442,10 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2369
2442
|
}
|
|
2370
2443
|
const row = await emailVerif.findByUser(ctx.user.id, pin);
|
|
2371
2444
|
if (!row) {
|
|
2372
|
-
return (0,
|
|
2445
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
2373
2446
|
}
|
|
2374
2447
|
if (/* @__PURE__ */ new Date() > row.expiresAt) {
|
|
2375
|
-
return (0,
|
|
2448
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
|
|
2376
2449
|
}
|
|
2377
2450
|
await store.transaction(async (tx) => {
|
|
2378
2451
|
await Promise.all([
|
|
@@ -2386,30 +2459,30 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2386
2459
|
])
|
|
2387
2460
|
]);
|
|
2388
2461
|
});
|
|
2389
|
-
return (0,
|
|
2462
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
2390
2463
|
verified: true,
|
|
2391
2464
|
email: ctx.user.email
|
|
2392
2465
|
});
|
|
2393
2466
|
},
|
|
2394
2467
|
sendVerification: async (ctx) => {
|
|
2395
2468
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
2396
|
-
return (0,
|
|
2469
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
2397
2470
|
}
|
|
2398
2471
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
2399
2472
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
2400
2473
|
if (ctx.user.loginMethod === "phone") {
|
|
2401
2474
|
const phone2 = ctx.user.phone;
|
|
2402
2475
|
if (!phone2) {
|
|
2403
|
-
return (0,
|
|
2404
|
-
|
|
2476
|
+
return (0, import_core7.setApiResponse)(
|
|
2477
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2405
2478
|
"NO_PHONE_ON_ACCOUNT",
|
|
2406
2479
|
"No phone number associated with this account"
|
|
2407
2480
|
);
|
|
2408
2481
|
}
|
|
2409
2482
|
const remaining2 = checkCooldown(await phoneVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
2410
2483
|
if (remaining2 > 0) {
|
|
2411
|
-
return (0,
|
|
2412
|
-
|
|
2484
|
+
return (0, import_core7.setApiResponse)(
|
|
2485
|
+
import_core7.HTTP.TOO_MANY_REQUESTS,
|
|
2413
2486
|
"VERIFICATION_COOLDOWN",
|
|
2414
2487
|
"Please wait before requesting a new code.",
|
|
2415
2488
|
{
|
|
@@ -2420,35 +2493,35 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2420
2493
|
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2421
2494
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
2422
2495
|
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
2423
|
-
await (0,
|
|
2496
|
+
await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
2424
2497
|
type: MESSAGE_KEYS.phoneOtp,
|
|
2425
2498
|
locale: ctx.user.locale,
|
|
2426
2499
|
data: { otp },
|
|
2427
2500
|
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
2428
2501
|
}));
|
|
2429
|
-
return (0,
|
|
2430
|
-
|
|
2502
|
+
return (0, import_core7.setApiResponse)(
|
|
2503
|
+
import_core7.HTTP.OK,
|
|
2431
2504
|
"VERIFICATION_SENT",
|
|
2432
2505
|
"A verification code has been sent to your phone."
|
|
2433
2506
|
);
|
|
2434
2507
|
}
|
|
2435
2508
|
if (!ctx.user.email) {
|
|
2436
|
-
return (0,
|
|
2437
|
-
|
|
2509
|
+
return (0, import_core7.setApiResponse)(
|
|
2510
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2438
2511
|
"NO_EMAIL_ON_ACCOUNT",
|
|
2439
2512
|
"No email address associated with this account"
|
|
2440
2513
|
);
|
|
2441
2514
|
}
|
|
2442
2515
|
if (ctx.user.emailVerifiedAt) {
|
|
2443
|
-
return (0,
|
|
2516
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "EMAIL_VERIFIED", "Email already verified.", {
|
|
2444
2517
|
verified: true,
|
|
2445
2518
|
email: ctx.user.email
|
|
2446
2519
|
});
|
|
2447
2520
|
}
|
|
2448
2521
|
const remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
2449
2522
|
if (remaining > 0) {
|
|
2450
|
-
return (0,
|
|
2451
|
-
|
|
2523
|
+
return (0, import_core7.setApiResponse)(
|
|
2524
|
+
import_core7.HTTP.TOO_MANY_REQUESTS,
|
|
2452
2525
|
"VERIFICATION_COOLDOWN",
|
|
2453
2526
|
"Please wait before requesting a new code.",
|
|
2454
2527
|
{
|
|
@@ -2459,13 +2532,13 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2459
2532
|
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2460
2533
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2461
2534
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2462
|
-
await (0,
|
|
2535
|
+
await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
2463
2536
|
type: MESSAGE_KEYS.emailVerification,
|
|
2464
2537
|
locale: ctx.user.locale,
|
|
2465
2538
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null },
|
|
2466
2539
|
data: { pin }
|
|
2467
2540
|
}));
|
|
2468
|
-
return (0,
|
|
2541
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
|
|
2469
2542
|
email: ctx.user.email
|
|
2470
2543
|
});
|
|
2471
2544
|
}
|
|
@@ -2474,11 +2547,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2474
2547
|
|
|
2475
2548
|
// src/controllers/user.controller.ts
|
|
2476
2549
|
var import_node_crypto7 = require("crypto");
|
|
2477
|
-
var
|
|
2550
|
+
var import_core9 = require("@fonderie/core");
|
|
2478
2551
|
var import_events3 = require("@fonderie/events");
|
|
2479
2552
|
|
|
2480
2553
|
// src/dtos/login-activity.ts
|
|
2481
|
-
var
|
|
2554
|
+
var import_core8 = require("@fonderie/core");
|
|
2482
2555
|
function toLoginEventDTO(row) {
|
|
2483
2556
|
return {
|
|
2484
2557
|
id: row.id,
|
|
@@ -2495,10 +2568,10 @@ function toLoginEventDTO(row) {
|
|
|
2495
2568
|
}
|
|
2496
2569
|
function toLoginHistoryPageDTO(page) {
|
|
2497
2570
|
const last = page.events[page.events.length - 1];
|
|
2498
|
-
const nextCursor = page.hasMore && last ? (0,
|
|
2571
|
+
const nextCursor = page.hasMore && last ? (0, import_core8.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
|
|
2499
2572
|
return { events: page.events.map(toLoginEventDTO), nextCursor };
|
|
2500
2573
|
}
|
|
2501
|
-
var decodeLoginCursor =
|
|
2574
|
+
var decodeLoginCursor = import_core8.decodeKeysetCursor;
|
|
2502
2575
|
function toSessionDTO(row, currentSid) {
|
|
2503
2576
|
return {
|
|
2504
2577
|
id: row.id,
|
|
@@ -2529,9 +2602,9 @@ function userController(store, config, bus) {
|
|
|
2529
2602
|
me: async (ctx) => {
|
|
2530
2603
|
const user = await users.findById(ctx.user.id);
|
|
2531
2604
|
if (!user) {
|
|
2532
|
-
return (0,
|
|
2605
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2533
2606
|
}
|
|
2534
|
-
return (0,
|
|
2607
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
|
|
2535
2608
|
user: toUserDTO(user, ctx.user.phoneVerified)
|
|
2536
2609
|
});
|
|
2537
2610
|
},
|
|
@@ -2542,14 +2615,14 @@ function userController(store, config, bus) {
|
|
|
2542
2615
|
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
2543
2616
|
const outcomeParam = params.get("outcome");
|
|
2544
2617
|
if (outcomeParam !== null && outcomeParam !== "success" && outcomeParam !== "failed") {
|
|
2545
|
-
return (0,
|
|
2618
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
|
|
2546
2619
|
}
|
|
2547
2620
|
const cursorParam = params.get("cursor");
|
|
2548
2621
|
let cursor = null;
|
|
2549
2622
|
if (cursorParam) {
|
|
2550
2623
|
cursor = decodeLoginCursor(cursorParam);
|
|
2551
2624
|
if (!cursor) {
|
|
2552
|
-
return (0,
|
|
2625
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
2553
2626
|
}
|
|
2554
2627
|
}
|
|
2555
2628
|
const query = { userId: ctx.user.id, limit };
|
|
@@ -2560,8 +2633,8 @@ function userController(store, config, bus) {
|
|
|
2560
2633
|
if (from) query.from = new Date(from);
|
|
2561
2634
|
if (to) query.to = new Date(to);
|
|
2562
2635
|
const page = await loginEvents.listByUser(query);
|
|
2563
|
-
return (0,
|
|
2564
|
-
|
|
2636
|
+
return (0, import_core9.setApiResponse)(
|
|
2637
|
+
import_core9.HTTP.OK,
|
|
2565
2638
|
"LOGIN_HISTORY_FETCHED",
|
|
2566
2639
|
"Login history retrieved.",
|
|
2567
2640
|
toLoginHistoryPageDTO(page)
|
|
@@ -2571,35 +2644,35 @@ function userController(store, config, bus) {
|
|
|
2571
2644
|
listSessions: async (ctx) => {
|
|
2572
2645
|
const rows = await sessions.listLiveByUser(ctx.user.id);
|
|
2573
2646
|
const currentSid = ctx.user.sid ?? null;
|
|
2574
|
-
return (0,
|
|
2647
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
|
|
2575
2648
|
sessions: rows.map((r) => toSessionDTO(r, currentSid))
|
|
2576
2649
|
});
|
|
2577
2650
|
},
|
|
2578
2651
|
terminateSession: async (ctx) => {
|
|
2579
2652
|
const id = ctx.meta["params"]?.["id"];
|
|
2580
2653
|
if (!id) {
|
|
2581
|
-
return (0,
|
|
2654
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2582
2655
|
}
|
|
2583
2656
|
const removed = await sessions.terminateByIdReturningSid(ctx.user.id, id);
|
|
2584
2657
|
if (removed === void 0) {
|
|
2585
|
-
return (0,
|
|
2658
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2586
2659
|
}
|
|
2587
|
-
await (0,
|
|
2588
|
-
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 });
|
|
2589
2662
|
},
|
|
2590
2663
|
terminateOtherSessions: async (ctx) => {
|
|
2591
2664
|
const currentSid = ctx.user.sid ?? null;
|
|
2592
2665
|
if (!currentSid) {
|
|
2593
|
-
return (0,
|
|
2594
|
-
|
|
2666
|
+
return (0, import_core9.setApiResponse)(
|
|
2667
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2595
2668
|
"NO_CURRENT_SESSION",
|
|
2596
2669
|
"Cannot terminate others without a current session context"
|
|
2597
2670
|
);
|
|
2598
2671
|
}
|
|
2599
2672
|
const sids = await sessions.terminateOthersReturningSids(ctx.user.id, currentSid);
|
|
2600
2673
|
const count = sids.length;
|
|
2601
|
-
if (count) await (0,
|
|
2602
|
-
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 });
|
|
2603
2676
|
},
|
|
2604
2677
|
updateProfile: async (ctx) => {
|
|
2605
2678
|
const body = ctx.meta["body"];
|
|
@@ -2609,18 +2682,18 @@ function userController(store, config, bus) {
|
|
|
2609
2682
|
if (body?.[key] !== void 0) fields[key] = body[key];
|
|
2610
2683
|
}
|
|
2611
2684
|
if (Object.keys(fields).length === 0) {
|
|
2612
|
-
return (0,
|
|
2613
|
-
|
|
2685
|
+
return (0, import_core9.setApiResponse)(
|
|
2686
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2614
2687
|
"INVALID_PARAMETER",
|
|
2615
2688
|
"Provide at least one of: firstName, lastName, avatarUrl"
|
|
2616
2689
|
);
|
|
2617
2690
|
}
|
|
2618
2691
|
const row = await users.update(ctx.user.id, fields);
|
|
2619
2692
|
if (!row) {
|
|
2620
|
-
return (0,
|
|
2693
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2621
2694
|
}
|
|
2622
2695
|
const updated = await users.findById(ctx.user.id);
|
|
2623
|
-
return (0,
|
|
2696
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "PROFILE_UPDATED", "Profile updated.", {
|
|
2624
2697
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2625
2698
|
});
|
|
2626
2699
|
},
|
|
@@ -2637,14 +2710,14 @@ function userController(store, config, bus) {
|
|
|
2637
2710
|
if (Object.keys(patch).length > 0) fields.patch = patch;
|
|
2638
2711
|
const row = await users.updatePreferences(ctx.user.id, fields);
|
|
2639
2712
|
if (!row) {
|
|
2640
|
-
return (0,
|
|
2641
|
-
|
|
2713
|
+
return (0, import_core9.setApiResponse)(
|
|
2714
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2642
2715
|
"INVALID_PARAMETER",
|
|
2643
2716
|
"Provide at least one preference field"
|
|
2644
2717
|
);
|
|
2645
2718
|
}
|
|
2646
2719
|
const updated = await users.findById(ctx.user.id);
|
|
2647
|
-
return (0,
|
|
2720
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
|
|
2648
2721
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2649
2722
|
});
|
|
2650
2723
|
},
|
|
@@ -2653,44 +2726,44 @@ function userController(store, config, bus) {
|
|
|
2653
2726
|
const newEmail = body?.["email"];
|
|
2654
2727
|
const normalised = typeof newEmail === "string" ? normalizeEmailSafe(newEmail) : null;
|
|
2655
2728
|
if (!normalised) {
|
|
2656
|
-
return (0,
|
|
2657
|
-
|
|
2729
|
+
return (0, import_core9.setApiResponse)(
|
|
2730
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2658
2731
|
"INVALID_PARAMETER",
|
|
2659
2732
|
"A valid email address is required"
|
|
2660
2733
|
);
|
|
2661
2734
|
}
|
|
2662
2735
|
const oldEmail = ctx.user.email;
|
|
2663
2736
|
if (normalised === oldEmail) {
|
|
2664
|
-
return (0,
|
|
2665
|
-
|
|
2737
|
+
return (0, import_core9.setApiResponse)(
|
|
2738
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2666
2739
|
"INVALID_PARAMETER",
|
|
2667
2740
|
"New email must differ from current email"
|
|
2668
2741
|
);
|
|
2669
2742
|
}
|
|
2670
2743
|
const existing = await users.findByEmail(normalised);
|
|
2671
2744
|
if (existing) {
|
|
2672
|
-
return (0,
|
|
2745
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
2673
2746
|
}
|
|
2674
2747
|
const pin = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
|
|
2675
2748
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2676
2749
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2677
2750
|
await users.updateEmail(ctx.user.id, normalised);
|
|
2678
|
-
await (0,
|
|
2751
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2679
2752
|
type: MESSAGE_KEYS.emailVerification,
|
|
2680
2753
|
locale: ctx.user.locale,
|
|
2681
2754
|
data: { pin },
|
|
2682
2755
|
recipient: { email: normalised, phone: null, deviceToken: null }
|
|
2683
2756
|
}));
|
|
2684
2757
|
if (oldEmail) {
|
|
2685
|
-
await (0,
|
|
2758
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2686
2759
|
type: MESSAGE_KEYS.emailChanged,
|
|
2687
2760
|
locale: ctx.user.locale,
|
|
2688
2761
|
data: { newEmail: normalised },
|
|
2689
2762
|
recipient: { email: oldEmail, phone: null, deviceToken: null }
|
|
2690
2763
|
}));
|
|
2691
2764
|
}
|
|
2692
|
-
return (0,
|
|
2693
|
-
|
|
2765
|
+
return (0, import_core9.setApiResponse)(
|
|
2766
|
+
import_core9.HTTP.OK,
|
|
2694
2767
|
"EMAIL_UPDATED",
|
|
2695
2768
|
"Email updated. A verification code has been sent to your new address.",
|
|
2696
2769
|
{
|
|
@@ -2702,8 +2775,8 @@ function userController(store, config, bus) {
|
|
|
2702
2775
|
const body = ctx.meta["body"];
|
|
2703
2776
|
const newPhone = body?.["phone"];
|
|
2704
2777
|
if (!isValidPhone2(newPhone)) {
|
|
2705
|
-
return (0,
|
|
2706
|
-
|
|
2778
|
+
return (0, import_core9.setApiResponse)(
|
|
2779
|
+
import_core9.HTTP.UNPROCESSABLE,
|
|
2707
2780
|
"INVALID_PARAMETER",
|
|
2708
2781
|
"A valid phone number is required"
|
|
2709
2782
|
);
|
|
@@ -2711,28 +2784,28 @@ function userController(store, config, bus) {
|
|
|
2711
2784
|
const normalised = normalizePhone2(newPhone);
|
|
2712
2785
|
const existing = await users.findByPhone(normalised);
|
|
2713
2786
|
if (existing) {
|
|
2714
|
-
return (0,
|
|
2787
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
2715
2788
|
}
|
|
2716
2789
|
const otp = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
|
|
2717
2790
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
2718
2791
|
await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
|
|
2719
2792
|
await users.updatePhone(ctx.user.id, normalised);
|
|
2720
|
-
await (0,
|
|
2793
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2721
2794
|
type: MESSAGE_KEYS.phoneOtp,
|
|
2722
2795
|
locale: ctx.user.locale,
|
|
2723
2796
|
data: { otp },
|
|
2724
2797
|
recipient: { email: null, phone: normalised, deviceToken: null }
|
|
2725
2798
|
}));
|
|
2726
2799
|
if (ctx.user.email) {
|
|
2727
|
-
await (0,
|
|
2800
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2728
2801
|
type: MESSAGE_KEYS.phoneChanged,
|
|
2729
2802
|
locale: ctx.user.locale,
|
|
2730
2803
|
data: {},
|
|
2731
2804
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
2732
2805
|
}));
|
|
2733
2806
|
}
|
|
2734
|
-
return (0,
|
|
2735
|
-
|
|
2807
|
+
return (0, import_core9.setApiResponse)(
|
|
2808
|
+
import_core9.HTTP.OK,
|
|
2736
2809
|
"PHONE_UPDATED",
|
|
2737
2810
|
"Phone number updated. A verification code has been sent to your new number.",
|
|
2738
2811
|
{
|
|
@@ -2745,23 +2818,23 @@ function userController(store, config, bus) {
|
|
|
2745
2818
|
const currentPassword = body?.["currentPassword"];
|
|
2746
2819
|
const newPassword = body?.["newPassword"];
|
|
2747
2820
|
if (typeof currentPassword !== "string" || !currentPassword) {
|
|
2748
|
-
return (0,
|
|
2821
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
|
|
2749
2822
|
}
|
|
2750
2823
|
if (typeof newPassword !== "string" || newPassword.length < 8) {
|
|
2751
|
-
return (0,
|
|
2824
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
|
|
2752
2825
|
}
|
|
2753
2826
|
const user = await users.findById(ctx.user.id);
|
|
2754
|
-
if (!user) return (0,
|
|
2827
|
+
if (!user) return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2755
2828
|
const { hashPassword: hashPassword2, verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password(), password_exports));
|
|
2756
2829
|
const valid = await verifyPassword2(currentPassword, user.passwordHash ?? "");
|
|
2757
2830
|
if (!valid) {
|
|
2758
|
-
return (0,
|
|
2831
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
|
|
2759
2832
|
}
|
|
2760
2833
|
const hash = await hashPassword2(newPassword);
|
|
2761
2834
|
await users.updatePassword(ctx.user.id, hash);
|
|
2762
2835
|
await sessions.deleteByUser(ctx.user.id);
|
|
2763
|
-
await (0,
|
|
2764
|
-
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.");
|
|
2765
2838
|
},
|
|
2766
2839
|
/**
|
|
2767
2840
|
* Remove an OAuth provider from the account.
|
|
@@ -2780,44 +2853,50 @@ function userController(store, config, bus) {
|
|
|
2780
2853
|
const provider = String(ctx.meta["params"]?.["provider"] ?? "");
|
|
2781
2854
|
const current = await users.findById(ctx.user.id);
|
|
2782
2855
|
if (!current?.provider) {
|
|
2783
|
-
return (0,
|
|
2856
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_LINKED", "No sign-in provider is linked.");
|
|
2784
2857
|
}
|
|
2785
2858
|
if (provider && provider.toLowerCase() !== current.provider.toLowerCase()) {
|
|
2786
|
-
return (0,
|
|
2787
|
-
|
|
2859
|
+
return (0, import_core9.setApiResponse)(
|
|
2860
|
+
import_core9.HTTP.NOT_FOUND,
|
|
2788
2861
|
"NOT_LINKED",
|
|
2789
2862
|
`This account is not linked to ${provider}.`
|
|
2790
2863
|
);
|
|
2791
2864
|
}
|
|
2792
2865
|
const removed = await users.clearProvider(ctx.user.id);
|
|
2793
2866
|
if (!removed) {
|
|
2794
|
-
return (0,
|
|
2795
|
-
|
|
2867
|
+
return (0, import_core9.setApiResponse)(
|
|
2868
|
+
import_core9.HTTP.CONFLICT,
|
|
2796
2869
|
"PASSWORD_REQUIRED",
|
|
2797
2870
|
"Set a password before disconnecting this provider \u2014 it is currently the only way to sign in."
|
|
2798
2871
|
);
|
|
2799
2872
|
}
|
|
2800
2873
|
if (ctx.user.email) {
|
|
2801
|
-
await (0,
|
|
2874
|
+
await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2802
2875
|
type: MESSAGE_KEYS.oauthUnlinked,
|
|
2803
2876
|
locale: ctx.user.locale,
|
|
2804
2877
|
data: { provider: current.provider },
|
|
2805
2878
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
2806
2879
|
}));
|
|
2807
2880
|
}
|
|
2808
|
-
return (0,
|
|
2881
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
|
|
2809
2882
|
},
|
|
2810
2883
|
deleteMe: async (ctx) => {
|
|
2811
2884
|
const userId = ctx.user.id;
|
|
2812
2885
|
await users.softDelete(userId);
|
|
2813
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
|
+
);
|
|
2814
2893
|
const reqId = ctx.meta["requestId"];
|
|
2815
|
-
await (0,
|
|
2894
|
+
await (0, import_core9.background)(bus?.emit(
|
|
2816
2895
|
EVENT_KEYS.userDeleted,
|
|
2817
2896
|
{ userId },
|
|
2818
2897
|
reqId !== void 0 ? { requestId: reqId } : void 0
|
|
2819
2898
|
));
|
|
2820
|
-
await (0,
|
|
2899
|
+
await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId, sids: null, reason: "account-deleted" }));
|
|
2821
2900
|
return Response.json(
|
|
2822
2901
|
{ reason: "ACCOUNT_DELETED", explanation: "Account successfully deleted." },
|
|
2823
2902
|
{
|
|
@@ -2831,7 +2910,7 @@ function userController(store, config, bus) {
|
|
|
2831
2910
|
// no MFA secret, no session tokens (session metadata only).
|
|
2832
2911
|
exportMe: async (ctx) => {
|
|
2833
2912
|
const user = await users.findById(ctx.user.id);
|
|
2834
|
-
if (!user) return (0,
|
|
2913
|
+
if (!user) return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2835
2914
|
const sessionRows = await sessions.listByUser(ctx.user.id);
|
|
2836
2915
|
const bundle = {
|
|
2837
2916
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2840,8 +2919,8 @@ function userController(store, config, bus) {
|
|
|
2840
2919
|
id: s.id,
|
|
2841
2920
|
userAgent: s.userAgent,
|
|
2842
2921
|
ipAddress: s.ipAddress,
|
|
2843
|
-
createdAt: (0,
|
|
2844
|
-
expiresAt: (0,
|
|
2922
|
+
createdAt: (0, import_core9.dateOrEmpty)(s.createdAt),
|
|
2923
|
+
expiresAt: (0, import_core9.dateOrEmpty)(s.expiresAt)
|
|
2845
2924
|
})),
|
|
2846
2925
|
security: {
|
|
2847
2926
|
mfaEnabled: user.mfaEnabled === true,
|
|
@@ -2860,7 +2939,7 @@ function userController(store, config, bus) {
|
|
|
2860
2939
|
}
|
|
2861
2940
|
bundle["modules"] = modules;
|
|
2862
2941
|
}
|
|
2863
|
-
return (0,
|
|
2942
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "DATA_EXPORT", "Your account data.", bundle);
|
|
2864
2943
|
}
|
|
2865
2944
|
};
|
|
2866
2945
|
}
|
|
@@ -2868,7 +2947,7 @@ function userController(store, config, bus) {
|
|
|
2868
2947
|
// src/controllers/oauth.controller.ts
|
|
2869
2948
|
var import_node_crypto9 = require("crypto");
|
|
2870
2949
|
var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
|
|
2871
|
-
var
|
|
2950
|
+
var import_core10 = require("@fonderie/core");
|
|
2872
2951
|
var import_events4 = require("@fonderie/events");
|
|
2873
2952
|
|
|
2874
2953
|
// src/services/jwks.ts
|
|
@@ -3019,7 +3098,7 @@ function oauthController(store, config, bus) {
|
|
|
3019
3098
|
const label = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
3020
3099
|
const recipient = { email: user.email, phone: null, deviceToken: null };
|
|
3021
3100
|
if (upserted.clearedUnverifiedPassword && user.email) {
|
|
3022
|
-
await (0,
|
|
3101
|
+
await (0, import_core10.background)(bus.emit(
|
|
3023
3102
|
import_events4.NOTIFICATION_EVENT,
|
|
3024
3103
|
{
|
|
3025
3104
|
type: MESSAGE_KEYS.passwordRevoked,
|
|
@@ -3031,7 +3110,7 @@ function oauthController(store, config, bus) {
|
|
|
3031
3110
|
));
|
|
3032
3111
|
}
|
|
3033
3112
|
if (upserted.inserted) {
|
|
3034
|
-
await (0,
|
|
3113
|
+
await (0, import_core10.background)(bus.emit(
|
|
3035
3114
|
EVENT_KEYS.userRegistered,
|
|
3036
3115
|
{
|
|
3037
3116
|
userId: upserted.id,
|
|
@@ -3043,7 +3122,7 @@ function oauthController(store, config, bus) {
|
|
|
3043
3122
|
reqOpts
|
|
3044
3123
|
));
|
|
3045
3124
|
if (user.email) {
|
|
3046
|
-
await (0,
|
|
3125
|
+
await (0, import_core10.background)(bus.emit(
|
|
3047
3126
|
import_events4.NOTIFICATION_EVENT,
|
|
3048
3127
|
{
|
|
3049
3128
|
type: MESSAGE_KEYS.oauthRegistration,
|
|
@@ -3057,7 +3136,7 @@ function oauthController(store, config, bus) {
|
|
|
3057
3136
|
return;
|
|
3058
3137
|
}
|
|
3059
3138
|
if (upserted.previousProvider !== provider && user.email) {
|
|
3060
|
-
await (0,
|
|
3139
|
+
await (0, import_core10.background)(bus.emit(
|
|
3061
3140
|
import_events4.NOTIFICATION_EVENT,
|
|
3062
3141
|
{
|
|
3063
3142
|
type: MESSAGE_KEYS.oauthLinked,
|
|
@@ -3078,14 +3157,29 @@ function oauthController(store, config, bus) {
|
|
|
3078
3157
|
const method = provider === "apple" ? "oauth-apple" : "oauth-google";
|
|
3079
3158
|
const meta = requestMeta(ctx);
|
|
3080
3159
|
if (!claims.email) {
|
|
3081
|
-
return (0,
|
|
3160
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, p.failed, `No email in ${p.label} identity token`);
|
|
3082
3161
|
}
|
|
3083
3162
|
if (!claims.emailVerified) {
|
|
3084
|
-
return (0,
|
|
3163
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, p.failed, `${p.label} account email is not verified`);
|
|
3085
3164
|
}
|
|
3086
3165
|
const normalizedEmail = normalizeEmailSafe(claims.email);
|
|
3087
3166
|
if (!normalizedEmail) {
|
|
3088
|
-
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 });
|
|
3089
3183
|
}
|
|
3090
3184
|
const upserted = await users.upsertByProvider(normalizedEmail, provider, claims.sub ?? "");
|
|
3091
3185
|
if (!upserted) {
|
|
@@ -3097,15 +3191,15 @@ function oauthController(store, config, bus) {
|
|
|
3097
3191
|
failureReason: "provider_upsert_failed",
|
|
3098
3192
|
...meta
|
|
3099
3193
|
});
|
|
3100
|
-
return (0,
|
|
3194
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
|
|
3101
3195
|
}
|
|
3102
3196
|
const fullUser = await users.findById(upserted.id);
|
|
3103
3197
|
if (!fullUser) {
|
|
3104
|
-
return (0,
|
|
3198
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
|
|
3105
3199
|
}
|
|
3106
3200
|
await announceOAuthUpsert(ctx, upserted, provider, fullUser);
|
|
3107
3201
|
if (fullUser.suspended) {
|
|
3108
|
-
return (0,
|
|
3202
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.FORBIDDEN, "ACCOUNT_SUSPENDED", "Account suspended. Please contact support.");
|
|
3109
3203
|
}
|
|
3110
3204
|
if (fullUser.mfaEnabled) {
|
|
3111
3205
|
const mfaToken = issueMfaPendingToken(upserted.id, config, provider);
|
|
@@ -3148,8 +3242,8 @@ function oauthController(store, config, bus) {
|
|
|
3148
3242
|
googleInit: async (_ctx) => {
|
|
3149
3243
|
const google = config.google;
|
|
3150
3244
|
if (!google || !google.clientSecret || !google.redirectUri) {
|
|
3151
|
-
return (0,
|
|
3152
|
-
|
|
3245
|
+
return (0, import_core10.setApiResponse)(
|
|
3246
|
+
import_core10.HTTP.NOT_IMPLEMENTED,
|
|
3153
3247
|
"NOT_CONFIGURED",
|
|
3154
3248
|
"Google OAuth not configured"
|
|
3155
3249
|
);
|
|
@@ -3183,8 +3277,8 @@ function oauthController(store, config, bus) {
|
|
|
3183
3277
|
googleCallback: async (ctx) => {
|
|
3184
3278
|
const google = config.google;
|
|
3185
3279
|
if (!google || !google.clientSecret || !google.redirectUri) {
|
|
3186
|
-
return (0,
|
|
3187
|
-
|
|
3280
|
+
return (0, import_core10.setApiResponse)(
|
|
3281
|
+
import_core10.HTTP.NOT_IMPLEMENTED,
|
|
3188
3282
|
"NOT_CONFIGURED",
|
|
3189
3283
|
"Google OAuth not configured"
|
|
3190
3284
|
);
|
|
@@ -3192,14 +3286,14 @@ function oauthController(store, config, bus) {
|
|
|
3192
3286
|
const url = new URL(ctx.request.url);
|
|
3193
3287
|
const code = url.searchParams.get("code");
|
|
3194
3288
|
if (!code) {
|
|
3195
|
-
return (0,
|
|
3289
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
3196
3290
|
}
|
|
3197
3291
|
const returnedState = url.searchParams.get("state") ?? "";
|
|
3198
3292
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
3199
3293
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
3200
|
-
if (!returnedState || !expectedState || !(0,
|
|
3201
|
-
return (0,
|
|
3202
|
-
|
|
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,
|
|
3203
3297
|
"GOOGLE_AUTH_FAILED",
|
|
3204
3298
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
3205
3299
|
);
|
|
@@ -3217,8 +3311,8 @@ function oauthController(store, config, bus) {
|
|
|
3217
3311
|
});
|
|
3218
3312
|
const tokenData = await tokenRes.json();
|
|
3219
3313
|
if (!tokenData.id_token) {
|
|
3220
|
-
return (0,
|
|
3221
|
-
|
|
3314
|
+
return (0, import_core10.setApiResponse)(
|
|
3315
|
+
import_core10.HTTP.BAD_REQUEST,
|
|
3222
3316
|
"GOOGLE_AUTH_FAILED",
|
|
3223
3317
|
"OAuth token exchange failed"
|
|
3224
3318
|
);
|
|
@@ -3227,20 +3321,20 @@ function oauthController(store, config, bus) {
|
|
|
3227
3321
|
Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
|
|
3228
3322
|
);
|
|
3229
3323
|
if (payload.aud !== google.clientId) {
|
|
3230
|
-
return (0,
|
|
3324
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
|
|
3231
3325
|
}
|
|
3232
3326
|
if (payload.iss !== "https://accounts.google.com" && payload.iss !== "accounts.google.com") {
|
|
3233
|
-
return (0,
|
|
3327
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
|
|
3234
3328
|
}
|
|
3235
3329
|
if (typeof payload.exp !== "number" || payload.exp * 1e3 < Date.now()) {
|
|
3236
|
-
return (0,
|
|
3330
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
|
|
3237
3331
|
}
|
|
3238
3332
|
if (!payload.email) {
|
|
3239
|
-
return (0,
|
|
3333
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
|
|
3240
3334
|
}
|
|
3241
3335
|
if (payload.email_verified !== true) {
|
|
3242
|
-
return (0,
|
|
3243
|
-
|
|
3336
|
+
return (0, import_core10.setApiResponse)(
|
|
3337
|
+
import_core10.HTTP.BAD_REQUEST,
|
|
3244
3338
|
"GOOGLE_AUTH_FAILED",
|
|
3245
3339
|
"Google account email is not verified"
|
|
3246
3340
|
);
|
|
@@ -3254,7 +3348,7 @@ function oauthController(store, config, bus) {
|
|
|
3254
3348
|
appleInit: async (_ctx) => {
|
|
3255
3349
|
const apple = config.apple;
|
|
3256
3350
|
if (!apple) {
|
|
3257
|
-
return (0,
|
|
3351
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3258
3352
|
}
|
|
3259
3353
|
const state = (0, import_node_crypto9.randomBytes)(16).toString("hex");
|
|
3260
3354
|
const params = new URLSearchParams({
|
|
@@ -3285,20 +3379,20 @@ function oauthController(store, config, bus) {
|
|
|
3285
3379
|
appleCallback: async (ctx) => {
|
|
3286
3380
|
const apple = config.apple;
|
|
3287
3381
|
if (!apple) {
|
|
3288
|
-
return (0,
|
|
3382
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3289
3383
|
}
|
|
3290
3384
|
const pre = ctx.meta["body"];
|
|
3291
3385
|
const form = pre && typeof pre === "object" ? new URLSearchParams(pre) : new URLSearchParams(await ctx.request.text());
|
|
3292
3386
|
const code = form.get("code");
|
|
3293
3387
|
if (!code) {
|
|
3294
|
-
return (0,
|
|
3388
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
3295
3389
|
}
|
|
3296
3390
|
const returnedState = form.get("state") ?? "";
|
|
3297
3391
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
3298
3392
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
3299
|
-
if (!returnedState || !expectedState || !(0,
|
|
3300
|
-
return (0,
|
|
3301
|
-
|
|
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,
|
|
3302
3396
|
"APPLE_AUTH_FAILED",
|
|
3303
3397
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
3304
3398
|
);
|
|
@@ -3316,11 +3410,11 @@ function oauthController(store, config, bus) {
|
|
|
3316
3410
|
});
|
|
3317
3411
|
const tokenData = await tokenRes.json();
|
|
3318
3412
|
if (!tokenData.id_token) {
|
|
3319
|
-
return (0,
|
|
3413
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
|
|
3320
3414
|
}
|
|
3321
3415
|
const claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });
|
|
3322
3416
|
if (!claims) {
|
|
3323
|
-
return (0,
|
|
3417
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3324
3418
|
}
|
|
3325
3419
|
return completeOAuthLogin(ctx, "apple", {
|
|
3326
3420
|
email: claims.email,
|
|
@@ -3331,12 +3425,12 @@ function oauthController(store, config, bus) {
|
|
|
3331
3425
|
appleNative: async (ctx) => {
|
|
3332
3426
|
const apple = config.apple;
|
|
3333
3427
|
if (!apple) {
|
|
3334
|
-
return (0,
|
|
3428
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3335
3429
|
}
|
|
3336
3430
|
const body = ctx.meta["body"];
|
|
3337
3431
|
const identityToken = body?.identityToken;
|
|
3338
3432
|
if (!identityToken) {
|
|
3339
|
-
return (0,
|
|
3433
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
|
|
3340
3434
|
}
|
|
3341
3435
|
const audiences = apple.nativeClientIds && apple.nativeClientIds.length ? apple.nativeClientIds : [apple.clientId];
|
|
3342
3436
|
const claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });
|
|
@@ -3350,7 +3444,7 @@ function oauthController(store, config, bus) {
|
|
|
3350
3444
|
failureReason: "invalid_identity_token",
|
|
3351
3445
|
...meta
|
|
3352
3446
|
});
|
|
3353
|
-
return (0,
|
|
3447
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3354
3448
|
}
|
|
3355
3449
|
const tokenHash = (0, import_node_crypto9.createHash)("sha256").update(identityToken).digest("hex");
|
|
3356
3450
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
@@ -3365,8 +3459,8 @@ function oauthController(store, config, bus) {
|
|
|
3365
3459
|
failureReason: "token_replayed",
|
|
3366
3460
|
...meta
|
|
3367
3461
|
});
|
|
3368
|
-
return (0,
|
|
3369
|
-
|
|
3462
|
+
return (0, import_core10.setApiResponse)(
|
|
3463
|
+
import_core10.HTTP.UNAUTHORIZED,
|
|
3370
3464
|
"APPLE_AUTH_FAILED",
|
|
3371
3465
|
"This Apple identity token has already been used"
|
|
3372
3466
|
);
|
|
@@ -3383,12 +3477,12 @@ function oauthController(store, config, bus) {
|
|
|
3383
3477
|
googleNative: async (ctx) => {
|
|
3384
3478
|
const audiences = config.google?.nativeClientIds ?? [];
|
|
3385
3479
|
if (!config.google || audiences.length === 0) {
|
|
3386
|
-
return (0,
|
|
3480
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Native Google sign-in not configured");
|
|
3387
3481
|
}
|
|
3388
3482
|
const body = ctx.meta["body"];
|
|
3389
3483
|
const idToken = body?.idToken;
|
|
3390
3484
|
if (!idToken) {
|
|
3391
|
-
return (0,
|
|
3485
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing idToken");
|
|
3392
3486
|
}
|
|
3393
3487
|
const claims = await verifyGoogleIdToken(idToken, { audiences, nonce: body?.nonce });
|
|
3394
3488
|
if (!claims) {
|
|
@@ -3400,7 +3494,7 @@ function oauthController(store, config, bus) {
|
|
|
3400
3494
|
failureReason: "invalid_identity_token",
|
|
3401
3495
|
...requestMeta(ctx)
|
|
3402
3496
|
});
|
|
3403
|
-
return (0,
|
|
3497
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "Invalid Google ID token");
|
|
3404
3498
|
}
|
|
3405
3499
|
const tokenHash = (0, import_node_crypto9.createHash)("sha256").update(idToken).digest("hex");
|
|
3406
3500
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
@@ -3413,7 +3507,7 @@ function oauthController(store, config, bus) {
|
|
|
3413
3507
|
failureReason: "token_replayed",
|
|
3414
3508
|
...requestMeta(ctx)
|
|
3415
3509
|
});
|
|
3416
|
-
return (0,
|
|
3510
|
+
return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "This Google ID token has already been used");
|
|
3417
3511
|
}
|
|
3418
3512
|
consumedTokens.sweepExpiredSafe();
|
|
3419
3513
|
return completeOAuthLogin(ctx, "google", {
|
|
@@ -3459,7 +3553,7 @@ function buildAuthRoutes(store, config, bus, locales) {
|
|
|
3459
3553
|
"providers",
|
|
3460
3554
|
"GET",
|
|
3461
3555
|
"/auth/providers",
|
|
3462
|
-
async () => (0,
|
|
3556
|
+
async () => (0, import_core11.setApiResponse)(import_core11.HTTP.OK, "AUTH_PROVIDERS", "Sign-in methods available here", {
|
|
3463
3557
|
providers: [...config.providers]
|
|
3464
3558
|
})
|
|
3465
3559
|
),
|
|
@@ -3534,20 +3628,20 @@ function buildAuthRoutes(store, config, bus, locales) {
|
|
|
3534
3628
|
}
|
|
3535
3629
|
|
|
3536
3630
|
// src/services/config-guard.ts
|
|
3537
|
-
var
|
|
3631
|
+
var import_core12 = require("@fonderie/core");
|
|
3538
3632
|
var MODULE = "@fonderie/auth";
|
|
3539
3633
|
function collectAuthConfigProblems(config) {
|
|
3540
3634
|
const problems = [];
|
|
3541
3635
|
const secret = config.jwtSecret ?? "";
|
|
3542
|
-
const secretProblem = (0,
|
|
3636
|
+
const secretProblem = (0, import_core12.secretStrengthProblem)(secret);
|
|
3543
3637
|
if (secretProblem === "too-short") {
|
|
3544
3638
|
problems.push({
|
|
3545
3639
|
module: MODULE,
|
|
3546
3640
|
severity: "error",
|
|
3547
|
-
message: `jwtSecret must be at least ${
|
|
3641
|
+
message: `jwtSecret must be at least ${import_core12.MIN_SECRET_LENGTH} characters (got ${secret.length})`,
|
|
3548
3642
|
domain: "auth",
|
|
3549
3643
|
reason: "JWT_SECRET_TOO_SHORT",
|
|
3550
|
-
metadata: { min:
|
|
3644
|
+
metadata: { min: import_core12.MIN_SECRET_LENGTH, got: secret.length }
|
|
3551
3645
|
});
|
|
3552
3646
|
} else if (secretProblem === "placeholder") {
|
|
3553
3647
|
problems.push({
|
|
@@ -3559,7 +3653,7 @@ function collectAuthConfigProblems(config) {
|
|
|
3559
3653
|
});
|
|
3560
3654
|
}
|
|
3561
3655
|
(config.jwtPreviousSecrets ?? []).forEach((previous, index) => {
|
|
3562
|
-
if ((0,
|
|
3656
|
+
if ((0, import_core12.secretStrengthProblem)(previous ?? "")) {
|
|
3563
3657
|
problems.push({
|
|
3564
3658
|
module: MODULE,
|
|
3565
3659
|
severity: "error",
|
|
@@ -3592,7 +3686,7 @@ function collectAuthConfigProblems(config) {
|
|
|
3592
3686
|
domain: "auth",
|
|
3593
3687
|
reason: "GOOGLE_INCOMPLETE"
|
|
3594
3688
|
});
|
|
3595
|
-
} else if (clientSecret &&
|
|
3689
|
+
} else if (clientSecret && import_core12.PLACEHOLDER_SECRET.test(clientSecret)) {
|
|
3596
3690
|
problems.push({
|
|
3597
3691
|
module: MODULE,
|
|
3598
3692
|
severity: "error",
|
|
@@ -3723,8 +3817,8 @@ function extractToken(request) {
|
|
|
3723
3817
|
}
|
|
3724
3818
|
|
|
3725
3819
|
// src/admin.ts
|
|
3726
|
-
var import_core12 = require("@fonderie/core");
|
|
3727
3820
|
var import_core13 = require("@fonderie/core");
|
|
3821
|
+
var import_core14 = require("@fonderie/core");
|
|
3728
3822
|
function toAdminUserDTO(user) {
|
|
3729
3823
|
return {
|
|
3730
3824
|
...toUserDTO(user),
|
|
@@ -3735,12 +3829,12 @@ function toAdminUserDTO(user) {
|
|
|
3735
3829
|
}
|
|
3736
3830
|
function toAdminUserPageDTO(page) {
|
|
3737
3831
|
const last = page.users[page.users.length - 1];
|
|
3738
|
-
const nextCursor = page.hasMore && last ? (0,
|
|
3832
|
+
const nextCursor = page.hasMore && last ? (0, import_core13.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
|
|
3739
3833
|
return { users: page.users.map(toAdminUserDTO), nextCursor };
|
|
3740
3834
|
}
|
|
3741
3835
|
var ADMIN_PREFIX = "/_admin";
|
|
3742
3836
|
var idOf = (ctx) => ctx.meta.params?.["id"] ?? "";
|
|
3743
|
-
var NOT_FOUND = () => (0,
|
|
3837
|
+
var NOT_FOUND = () => (0, import_core13.setApiResponse)(import_core13.HTTP.NOT_FOUND, "NOT_FOUND", "No such user");
|
|
3744
3838
|
function adminRouteTable(store, bus) {
|
|
3745
3839
|
const users = new UserModel(store);
|
|
3746
3840
|
const sessions = new SessionModel(store);
|
|
@@ -3749,8 +3843,8 @@ function adminRouteTable(store, bus) {
|
|
|
3749
3843
|
const ok = await users.setSuspended(idOf(ctx), suspended);
|
|
3750
3844
|
if (!ok) return NOT_FOUND();
|
|
3751
3845
|
const user = await users.findById(idOf(ctx));
|
|
3752
|
-
return user ? (0,
|
|
3753
|
-
|
|
3846
|
+
return user ? (0, import_core13.setApiResponse)(
|
|
3847
|
+
import_core13.HTTP.OK,
|
|
3754
3848
|
suspended ? "USER_SUSPENDED" : "USER_UNSUSPENDED",
|
|
3755
3849
|
suspended ? "User suspended" : "User unsuspended",
|
|
3756
3850
|
toAdminUserDTO(user)
|
|
@@ -3769,21 +3863,21 @@ function adminRouteTable(store, bus) {
|
|
|
3769
3863
|
const email2 = raw ? normalizeEmailSafe(raw) ?? raw.toLowerCase() : void 0;
|
|
3770
3864
|
if (email2) {
|
|
3771
3865
|
const user = await users.findByEmail(email2);
|
|
3772
|
-
return user ? (0,
|
|
3866
|
+
return user ? (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
|
|
3773
3867
|
}
|
|
3774
3868
|
const rawLimit = Number(params.get("limit") ?? 50);
|
|
3775
3869
|
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
3776
3870
|
const cursorParam = params.get("cursor");
|
|
3777
3871
|
const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
|
|
3778
3872
|
if (cursorParam && !cursor)
|
|
3779
|
-
return (0,
|
|
3873
|
+
return (0, import_core13.setApiResponse)(import_core13.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
3780
3874
|
const deleted = params.get("deleted") === "1" || params.get("deleted") === "true";
|
|
3781
3875
|
const page = await users.list({
|
|
3782
3876
|
limit,
|
|
3783
3877
|
...cursor ? { cursor } : {},
|
|
3784
3878
|
...deleted ? { deleted } : {}
|
|
3785
3879
|
});
|
|
3786
|
-
return (0,
|
|
3880
|
+
return (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "USERS", "Users", toAdminUserPageDTO(page));
|
|
3787
3881
|
}
|
|
3788
3882
|
],
|
|
3789
3883
|
[
|
|
@@ -3791,7 +3885,7 @@ function adminRouteTable(store, bus) {
|
|
|
3791
3885
|
"/_admin/users/:id",
|
|
3792
3886
|
async (ctx) => {
|
|
3793
3887
|
const user = await users.findByIdIncludingDeleted(idOf(ctx));
|
|
3794
|
-
return user ? (0,
|
|
3888
|
+
return user ? (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
|
|
3795
3889
|
}
|
|
3796
3890
|
],
|
|
3797
3891
|
[
|
|
@@ -3800,8 +3894,8 @@ function adminRouteTable(store, bus) {
|
|
|
3800
3894
|
async (ctx) => {
|
|
3801
3895
|
if (!await users.findById(idOf(ctx))) return NOT_FOUND();
|
|
3802
3896
|
const rows = await sessions.listLiveByUser(idOf(ctx));
|
|
3803
|
-
return (0,
|
|
3804
|
-
|
|
3897
|
+
return (0, import_core13.setApiResponse)(
|
|
3898
|
+
import_core13.HTTP.OK,
|
|
3805
3899
|
"SESSIONS",
|
|
3806
3900
|
"Live sessions",
|
|
3807
3901
|
rows.map((r) => toSessionDTO(r, null))
|
|
@@ -3815,8 +3909,8 @@ function adminRouteTable(store, bus) {
|
|
|
3815
3909
|
async (ctx) => {
|
|
3816
3910
|
if (!await users.findById(idOf(ctx))) return NOT_FOUND();
|
|
3817
3911
|
await sessions.deleteByUser(idOf(ctx));
|
|
3818
|
-
await (0,
|
|
3819
|
-
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");
|
|
3820
3914
|
}
|
|
3821
3915
|
],
|
|
3822
3916
|
[
|
|
@@ -3830,14 +3924,14 @@ function adminRouteTable(store, bus) {
|
|
|
3830
3924
|
const cursorParam = params.get("cursor");
|
|
3831
3925
|
const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
|
|
3832
3926
|
if (cursorParam && !cursor)
|
|
3833
|
-
return (0,
|
|
3927
|
+
return (0, import_core13.setApiResponse)(import_core13.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
3834
3928
|
const page = await events.listByUser({
|
|
3835
3929
|
userId: idOf(ctx),
|
|
3836
3930
|
limit,
|
|
3837
3931
|
...cursor ? { cursor } : {}
|
|
3838
3932
|
});
|
|
3839
|
-
return (0,
|
|
3840
|
-
|
|
3933
|
+
return (0, import_core13.setApiResponse)(
|
|
3934
|
+
import_core13.HTTP.OK,
|
|
3841
3935
|
"LOGIN_HISTORY",
|
|
3842
3936
|
"Login history",
|
|
3843
3937
|
toLoginHistoryPageDTO(page)
|
|
@@ -3870,7 +3964,7 @@ var AuthModule = class {
|
|
|
3870
3964
|
name = "@fonderie/auth";
|
|
3871
3965
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3872
3966
|
// say what is actually deployed rather than 'not reported'.
|
|
3873
|
-
version = "7.
|
|
3967
|
+
version = "7.23.0";
|
|
3874
3968
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3875
3969
|
describeAdmin() {
|
|
3876
3970
|
return { routes: describeAuthAdminRoutes(this.store, this.bus) };
|
|
@@ -3901,7 +3995,7 @@ var AuthModule = class {
|
|
|
3901
3995
|
};
|
|
3902
3996
|
|
|
3903
3997
|
// src/templates.ts
|
|
3904
|
-
var
|
|
3998
|
+
var import_core15 = require("@fonderie/core");
|
|
3905
3999
|
|
|
3906
4000
|
// src/templates.es.ts
|
|
3907
4001
|
var ES_TEMPLATES = {
|
|
@@ -4686,7 +4780,7 @@ change your password \u2014 someone else may be able to sign in as you.`
|
|
|
4686
4780
|
If you did this, you're all set. If not, contact support right away.`
|
|
4687
4781
|
}
|
|
4688
4782
|
};
|
|
4689
|
-
var DEFAULT_TEMPLATES = (0,
|
|
4783
|
+
var DEFAULT_TEMPLATES = (0, import_core15.withTranslations)(EN_TEMPLATES, {
|
|
4690
4784
|
fr: FR_TEMPLATES,
|
|
4691
4785
|
es: ES_TEMPLATES,
|
|
4692
4786
|
"zh-Hans": ZH_HANS_TEMPLATES,
|