@fonderie/auth 7.3.4 → 7.3.6
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/dist/index.cjs +216 -214
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +40 -38
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -275,7 +275,7 @@ function cookieHeaders(cookies) {
|
|
|
275
275
|
|
|
276
276
|
// src/controllers/mfa.controller.ts
|
|
277
277
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
278
|
-
var
|
|
278
|
+
var import_core5 = require("@fonderie/core");
|
|
279
279
|
var import_events = require("@fonderie/events");
|
|
280
280
|
|
|
281
281
|
// src/config.ts
|
|
@@ -876,6 +876,7 @@ var BackupCodeModel = class {
|
|
|
876
876
|
};
|
|
877
877
|
|
|
878
878
|
// src/models/login-event.model.ts
|
|
879
|
+
var import_core4 = require("@fonderie/core");
|
|
879
880
|
var LoginEventModel = class {
|
|
880
881
|
constructor(store) {
|
|
881
882
|
this.store = store;
|
|
@@ -897,12 +898,13 @@ var LoginEventModel = class {
|
|
|
897
898
|
]
|
|
898
899
|
);
|
|
899
900
|
}
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
901
|
+
// Never throws — recording history must not fail a login. It IS awaited,
|
|
902
|
+
// though: this is a security audit trail (who signed in, from where), and a
|
|
903
|
+
// detached write is abandoned when a serverless instance freezes after the
|
|
904
|
+
// response, losing the row entirely. background() decides whether waiting is
|
|
905
|
+
// actually necessary for the current runtime.
|
|
906
|
+
async recordSafe(e) {
|
|
907
|
+
await (0, import_core4.background)(this.record(e));
|
|
906
908
|
}
|
|
907
909
|
// One page of a user's own login history, newest first. Keyset-paginated on
|
|
908
910
|
// (created_at, id) — mirrors audit's event list so the two log UIs share a
|
|
@@ -974,8 +976,8 @@ function mfaController(store, config, issuer, bus) {
|
|
|
974
976
|
users.saveMfaPendingSecret(ctx.user.id, mfaCipher.encrypt(secret)),
|
|
975
977
|
backupCodes.replace(ctx.user.id, codeHashes)
|
|
976
978
|
]);
|
|
977
|
-
return (0,
|
|
978
|
-
|
|
979
|
+
return (0, import_core5.setApiResponse)(
|
|
980
|
+
import_core5.HTTP.OK,
|
|
979
981
|
"MFA_SETUP_INITIATED",
|
|
980
982
|
"Scan the QR code with your authenticator app.",
|
|
981
983
|
{
|
|
@@ -992,7 +994,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
992
994
|
const token = body?.["token"];
|
|
993
995
|
const isLoginCompletion = ctx.user.mfaPending === true;
|
|
994
996
|
const meta = requestMeta(ctx);
|
|
995
|
-
const failedMfa = () => loginEvents.recordSafe({
|
|
997
|
+
const failedMfa = async () => await loginEvents.recordSafe({
|
|
996
998
|
userId: ctx.user.id,
|
|
997
999
|
emailAttempted: ctx.user.email,
|
|
998
1000
|
method: "mfa",
|
|
@@ -1001,34 +1003,34 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1001
1003
|
...meta
|
|
1002
1004
|
});
|
|
1003
1005
|
if (typeof token !== "string") {
|
|
1004
|
-
return (0,
|
|
1006
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "token is required");
|
|
1005
1007
|
}
|
|
1006
1008
|
const pendingCipher = await users.getMfaPendingSecret(ctx.user.id);
|
|
1007
1009
|
const pendingSecret = pendingCipher ? mfaCipher.decrypt(pendingCipher) : null;
|
|
1008
1010
|
if (pendingSecret) {
|
|
1009
1011
|
if (!verifyTotpToken(token, pendingSecret)) {
|
|
1010
|
-
return (0,
|
|
1012
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid MFA token");
|
|
1011
1013
|
}
|
|
1012
1014
|
await users.confirmMfaSecret(ctx.user.id);
|
|
1013
|
-
await (0,
|
|
1015
|
+
await (0, import_core5.background)(bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
1014
1016
|
type: MESSAGE_KEYS.mfaEnabled,
|
|
1015
1017
|
locale: ctx.user.locale,
|
|
1016
1018
|
data: {},
|
|
1017
1019
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
1018
1020
|
}));
|
|
1019
|
-
return (0,
|
|
1021
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "MFA_VERIFIED", "MFA verified successfully.", {
|
|
1020
1022
|
mfaEnabled: true
|
|
1021
1023
|
});
|
|
1022
1024
|
} else if (/^[A-Z0-9]{8}$/i.test(token)) {
|
|
1023
1025
|
if (!ctx.user.mfaPending) {
|
|
1024
|
-
return (0,
|
|
1025
|
-
|
|
1026
|
+
return (0, import_core5.setApiResponse)(
|
|
1027
|
+
import_core5.HTTP.FORBIDDEN,
|
|
1026
1028
|
"MFA_NOT_PENDING",
|
|
1027
1029
|
"Use the mfaToken from the login response"
|
|
1028
1030
|
);
|
|
1029
1031
|
}
|
|
1030
1032
|
if (!ctx.user.mfaEnabled) {
|
|
1031
|
-
return (0,
|
|
1033
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
|
|
1032
1034
|
}
|
|
1033
1035
|
const unused = await backupCodes.findUnused(ctx.user.id);
|
|
1034
1036
|
const checks = await Promise.all(
|
|
@@ -1039,26 +1041,26 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1039
1041
|
);
|
|
1040
1042
|
const matched = checks.find((r) => r.match);
|
|
1041
1043
|
if (!matched) {
|
|
1042
|
-
if (isLoginCompletion) failedMfa();
|
|
1043
|
-
return (0,
|
|
1044
|
+
if (isLoginCompletion) await failedMfa();
|
|
1045
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid backup code");
|
|
1044
1046
|
}
|
|
1045
1047
|
await backupCodes.consume(matched.id);
|
|
1046
1048
|
} else {
|
|
1047
1049
|
if (!ctx.user.mfaPending) {
|
|
1048
|
-
return (0,
|
|
1049
|
-
|
|
1050
|
+
return (0, import_core5.setApiResponse)(
|
|
1051
|
+
import_core5.HTTP.FORBIDDEN,
|
|
1050
1052
|
"MFA_NOT_PENDING",
|
|
1051
1053
|
"Use the mfaToken from the login response"
|
|
1052
1054
|
);
|
|
1053
1055
|
}
|
|
1054
1056
|
const storedSecret = await users.getMfaSecret(ctx.user.id);
|
|
1055
1057
|
if (!storedSecret) {
|
|
1056
|
-
return (0,
|
|
1058
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
|
|
1057
1059
|
}
|
|
1058
1060
|
const secret = mfaCipher.decrypt(storedSecret);
|
|
1059
1061
|
if (!verifyTotpToken(token, secret)) {
|
|
1060
|
-
if (isLoginCompletion) failedMfa();
|
|
1061
|
-
return (0,
|
|
1062
|
+
if (isLoginCompletion) await failedMfa();
|
|
1063
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid MFA token");
|
|
1062
1064
|
}
|
|
1063
1065
|
if (!ctx.user.mfaEnabled) {
|
|
1064
1066
|
await users.enableMfa(ctx.user.id);
|
|
@@ -1070,7 +1072,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1070
1072
|
});
|
|
1071
1073
|
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
1072
1074
|
if (isLoginCompletion) {
|
|
1073
|
-
loginEvents.recordSafe({
|
|
1075
|
+
await loginEvents.recordSafe({
|
|
1074
1076
|
userId: ctx.user.id,
|
|
1075
1077
|
emailAttempted: ctx.user.email,
|
|
1076
1078
|
method: "mfa",
|
|
@@ -1080,7 +1082,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1080
1082
|
}
|
|
1081
1083
|
const fullUser = await users.findById(ctx.user.id);
|
|
1082
1084
|
if (!fullUser) {
|
|
1083
|
-
return (0,
|
|
1085
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "User not found after MFA verify");
|
|
1084
1086
|
}
|
|
1085
1087
|
return Response.json(
|
|
1086
1088
|
{
|
|
@@ -1102,26 +1104,26 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1102
1104
|
const body = ctx.meta["body"];
|
|
1103
1105
|
const token = body?.["token"];
|
|
1104
1106
|
if (typeof token !== "string") {
|
|
1105
|
-
return (0,
|
|
1107
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
|
|
1106
1108
|
}
|
|
1107
1109
|
if (!ctx.user.mfaEnabled) {
|
|
1108
|
-
return (0,
|
|
1110
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
|
|
1109
1111
|
}
|
|
1110
1112
|
const storedSecret = await users.getMfaSecret(ctx.user.id);
|
|
1111
1113
|
const secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;
|
|
1112
1114
|
if (!secret || !verifyTotpToken(token, secret)) {
|
|
1113
|
-
return (0,
|
|
1115
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
|
|
1114
1116
|
}
|
|
1115
1117
|
const plainCodes = generateBackupCodes();
|
|
1116
1118
|
const codeHashes = await Promise.all(plainCodes.map((c) => hashPassword(c)));
|
|
1117
1119
|
await backupCodes.replace(ctx.user.id, codeHashes);
|
|
1118
|
-
await (0,
|
|
1120
|
+
await (0, import_core5.background)(bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
1119
1121
|
type: MESSAGE_KEYS.mfaBackupCodesRegenerated,
|
|
1120
1122
|
locale: ctx.user.locale,
|
|
1121
1123
|
data: {},
|
|
1122
1124
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
1123
1125
|
}));
|
|
1124
|
-
return (0,
|
|
1126
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "BACKUP_CODES_REGENERATED", "Backup codes regenerated.", {
|
|
1125
1127
|
backupCodes: plainCodes
|
|
1126
1128
|
});
|
|
1127
1129
|
},
|
|
@@ -1130,25 +1132,25 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1130
1132
|
const body = ctx.meta["body"];
|
|
1131
1133
|
const token = body?.["token"];
|
|
1132
1134
|
if (typeof token !== "string") {
|
|
1133
|
-
return (0,
|
|
1135
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
|
|
1134
1136
|
}
|
|
1135
1137
|
const user = await users.findById(ctx.user.id);
|
|
1136
1138
|
if (!user || !user.mfaEnabled) {
|
|
1137
|
-
return (0,
|
|
1139
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
|
|
1138
1140
|
}
|
|
1139
1141
|
const storedSecret = await users.getMfaSecret(ctx.user.id);
|
|
1140
1142
|
const secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;
|
|
1141
1143
|
if (!secret || !verifyTotpToken(token, secret)) {
|
|
1142
|
-
return (0,
|
|
1144
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
|
|
1143
1145
|
}
|
|
1144
1146
|
await Promise.all([users.disableMfa(ctx.user.id), backupCodes.deleteByUser(ctx.user.id)]);
|
|
1145
|
-
await (0,
|
|
1147
|
+
await (0, import_core5.background)(bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
1146
1148
|
type: MESSAGE_KEYS.mfaDisabled,
|
|
1147
1149
|
locale: user.locale,
|
|
1148
1150
|
data: {},
|
|
1149
1151
|
recipient: { email: user.email, phone: null, deviceToken: null }
|
|
1150
1152
|
}));
|
|
1151
|
-
return (0,
|
|
1153
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "MFA_DISABLED", "MFA disabled successfully.");
|
|
1152
1154
|
}
|
|
1153
1155
|
};
|
|
1154
1156
|
}
|
|
@@ -1156,7 +1158,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1156
1158
|
// src/controllers/auth.controller.ts
|
|
1157
1159
|
var import_node_crypto5 = require("crypto");
|
|
1158
1160
|
var import_events2 = require("@fonderie/events");
|
|
1159
|
-
var
|
|
1161
|
+
var import_core6 = require("@fonderie/core");
|
|
1160
1162
|
|
|
1161
1163
|
// src/services/cooldown.ts
|
|
1162
1164
|
function checkCooldown(lastSentAt, cooldownMs) {
|
|
@@ -1371,18 +1373,18 @@ function authController(store, config, bus) {
|
|
|
1371
1373
|
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
1372
1374
|
const normalizedEmail = normalizeEmailSafe(email2);
|
|
1373
1375
|
if (!normalizedEmail) {
|
|
1374
|
-
return (0,
|
|
1376
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1375
1377
|
}
|
|
1376
1378
|
if (password2.length < 8) {
|
|
1377
|
-
return (0,
|
|
1378
|
-
|
|
1379
|
+
return (0, import_core6.setApiResponse)(
|
|
1380
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1379
1381
|
"INVALID_PARAMETER",
|
|
1380
1382
|
"password must be at least 8 characters"
|
|
1381
1383
|
);
|
|
1382
1384
|
}
|
|
1383
1385
|
const existing = await users.findByEmail(normalizedEmail);
|
|
1384
1386
|
if (existing) {
|
|
1385
|
-
return (0,
|
|
1387
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
1386
1388
|
}
|
|
1387
1389
|
const passwordHash = await hashPassword(password2);
|
|
1388
1390
|
const row = await users.create(
|
|
@@ -1392,18 +1394,18 @@ function authController(store, config, bus) {
|
|
|
1392
1394
|
lastName
|
|
1393
1395
|
);
|
|
1394
1396
|
if (!row) {
|
|
1395
|
-
return (0,
|
|
1397
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1396
1398
|
}
|
|
1397
1399
|
const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1398
1400
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1399
1401
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1400
1402
|
const user = await users.findById(row.id);
|
|
1401
1403
|
if (!user) {
|
|
1402
|
-
return (0,
|
|
1404
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1403
1405
|
}
|
|
1404
1406
|
const reqId = ctx.meta["requestId"];
|
|
1405
1407
|
const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
|
|
1406
|
-
await (0,
|
|
1408
|
+
await (0, import_core6.background)(bus?.emit(
|
|
1407
1409
|
import_events2.NOTIFICATION_EVENT,
|
|
1408
1410
|
{
|
|
1409
1411
|
type: MESSAGE_KEYS.emailRegistration,
|
|
@@ -1413,7 +1415,7 @@ function authController(store, config, bus) {
|
|
|
1413
1415
|
},
|
|
1414
1416
|
reqOpts
|
|
1415
1417
|
));
|
|
1416
|
-
await (0,
|
|
1418
|
+
await (0, import_core6.background)(bus?.emit(
|
|
1417
1419
|
EVENT_KEYS.userRegistered,
|
|
1418
1420
|
{
|
|
1419
1421
|
userId: user.id,
|
|
@@ -1449,7 +1451,7 @@ function authController(store, config, bus) {
|
|
|
1449
1451
|
if (isValidPhone(phone2)) {
|
|
1450
1452
|
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
1451
1453
|
if (existing) {
|
|
1452
|
-
return (0,
|
|
1454
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
1453
1455
|
}
|
|
1454
1456
|
const { id } = await users.findOrCreateByPhone(
|
|
1455
1457
|
normalizePhone(phone2),
|
|
@@ -1461,11 +1463,11 @@ function authController(store, config, bus) {
|
|
|
1461
1463
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1462
1464
|
const user = await users.findById(id);
|
|
1463
1465
|
if (!user) {
|
|
1464
|
-
return (0,
|
|
1466
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1465
1467
|
}
|
|
1466
1468
|
const reqId2 = ctx.meta["requestId"];
|
|
1467
1469
|
const reqOpts2 = reqId2 !== void 0 ? { requestId: reqId2 } : void 0;
|
|
1468
|
-
await (0,
|
|
1470
|
+
await (0, import_core6.background)(bus?.emit(
|
|
1469
1471
|
import_events2.NOTIFICATION_EVENT,
|
|
1470
1472
|
{
|
|
1471
1473
|
type: MESSAGE_KEYS.phoneOtp,
|
|
@@ -1475,7 +1477,7 @@ function authController(store, config, bus) {
|
|
|
1475
1477
|
},
|
|
1476
1478
|
reqOpts2
|
|
1477
1479
|
));
|
|
1478
|
-
await (0,
|
|
1480
|
+
await (0, import_core6.background)(bus?.emit(
|
|
1479
1481
|
EVENT_KEYS.userRegistered,
|
|
1480
1482
|
{
|
|
1481
1483
|
userId: user.id,
|
|
@@ -1499,8 +1501,8 @@ function authController(store, config, bus) {
|
|
|
1499
1501
|
{ status: 202 }
|
|
1500
1502
|
);
|
|
1501
1503
|
}
|
|
1502
|
-
return (0,
|
|
1503
|
-
|
|
1504
|
+
return (0, import_core6.setApiResponse)(
|
|
1505
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1504
1506
|
"INVALID_PARAMETER",
|
|
1505
1507
|
"Provide email + password or a valid phone number"
|
|
1506
1508
|
);
|
|
@@ -1512,11 +1514,11 @@ function authController(store, config, bus) {
|
|
|
1512
1514
|
const { email: rawEmail, password: password2 } = body;
|
|
1513
1515
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
1514
1516
|
if (!email2) {
|
|
1515
|
-
return (0,
|
|
1517
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1516
1518
|
}
|
|
1517
1519
|
const user = await users.findByEmail(email2);
|
|
1518
1520
|
if (!user || !user.passwordHash) {
|
|
1519
|
-
loginEvents.recordSafe({
|
|
1521
|
+
await loginEvents.recordSafe({
|
|
1520
1522
|
userId: null,
|
|
1521
1523
|
emailAttempted: email2,
|
|
1522
1524
|
method: "password",
|
|
@@ -1524,7 +1526,7 @@ function authController(store, config, bus) {
|
|
|
1524
1526
|
failureReason: "unknown_email",
|
|
1525
1527
|
...meta
|
|
1526
1528
|
});
|
|
1527
|
-
return (0,
|
|
1529
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1528
1530
|
}
|
|
1529
1531
|
const { valid, needsRehash } = await verifyPasswordForLogin(
|
|
1530
1532
|
password2,
|
|
@@ -1532,7 +1534,7 @@ function authController(store, config, bus) {
|
|
|
1532
1534
|
config.legacyVerify
|
|
1533
1535
|
);
|
|
1534
1536
|
if (!valid) {
|
|
1535
|
-
loginEvents.recordSafe({
|
|
1537
|
+
await loginEvents.recordSafe({
|
|
1536
1538
|
userId: user.id,
|
|
1537
1539
|
emailAttempted: email2,
|
|
1538
1540
|
method: "password",
|
|
@@ -1540,21 +1542,21 @@ function authController(store, config, bus) {
|
|
|
1540
1542
|
failureReason: "bad_password",
|
|
1541
1543
|
...meta
|
|
1542
1544
|
});
|
|
1543
|
-
return (0,
|
|
1545
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1544
1546
|
}
|
|
1545
1547
|
if (needsRehash) {
|
|
1546
1548
|
await users.updatePassword(user.id, await hashPassword(password2));
|
|
1547
1549
|
}
|
|
1548
1550
|
if (user.suspended) {
|
|
1549
|
-
return (0,
|
|
1550
|
-
|
|
1551
|
+
return (0, import_core6.setApiResponse)(
|
|
1552
|
+
import_core6.HTTP.FORBIDDEN,
|
|
1551
1553
|
"ACCOUNT_SUSPENDED",
|
|
1552
1554
|
"Account suspended. Please contact support."
|
|
1553
1555
|
);
|
|
1554
1556
|
}
|
|
1555
1557
|
if (user.mfaEnabled) {
|
|
1556
1558
|
const mfaToken = issueMfaPendingToken(user.id, config, "email");
|
|
1557
|
-
return (0,
|
|
1559
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
|
|
1558
1560
|
mfaToken
|
|
1559
1561
|
});
|
|
1560
1562
|
}
|
|
@@ -1562,7 +1564,7 @@ function authController(store, config, bus) {
|
|
|
1562
1564
|
loginMethod: "email"
|
|
1563
1565
|
});
|
|
1564
1566
|
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
1565
|
-
loginEvents.recordSafe({
|
|
1567
|
+
await loginEvents.recordSafe({
|
|
1566
1568
|
userId: user.id,
|
|
1567
1569
|
emailAttempted: email2,
|
|
1568
1570
|
method: "password",
|
|
@@ -1591,11 +1593,11 @@ function authController(store, config, bus) {
|
|
|
1591
1593
|
if (isValidPhone(phone2)) {
|
|
1592
1594
|
const user = await users.findByPhone(normalizePhone(phone2));
|
|
1593
1595
|
if (!user) {
|
|
1594
|
-
return (0,
|
|
1596
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1595
1597
|
}
|
|
1596
1598
|
if (user.suspended) {
|
|
1597
|
-
return (0,
|
|
1598
|
-
|
|
1599
|
+
return (0, import_core6.setApiResponse)(
|
|
1600
|
+
import_core6.HTTP.FORBIDDEN,
|
|
1599
1601
|
"ACCOUNT_SUSPENDED",
|
|
1600
1602
|
"Account suspended. Please contact support."
|
|
1601
1603
|
);
|
|
@@ -1603,7 +1605,7 @@ function authController(store, config, bus) {
|
|
|
1603
1605
|
const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1604
1606
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1605
1607
|
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1606
|
-
await (0,
|
|
1608
|
+
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1607
1609
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1608
1610
|
locale: user.locale,
|
|
1609
1611
|
data: { otp },
|
|
@@ -1619,8 +1621,8 @@ function authController(store, config, bus) {
|
|
|
1619
1621
|
{ status: 202 }
|
|
1620
1622
|
);
|
|
1621
1623
|
}
|
|
1622
|
-
return (0,
|
|
1623
|
-
|
|
1624
|
+
return (0, import_core6.setApiResponse)(
|
|
1625
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1624
1626
|
"INVALID_PARAMETER",
|
|
1625
1627
|
"Provide email + password or a valid phone number"
|
|
1626
1628
|
);
|
|
@@ -1645,23 +1647,23 @@ function authController(store, config, bus) {
|
|
|
1645
1647
|
refresh: async (ctx) => {
|
|
1646
1648
|
const token = extractRefreshToken(ctx);
|
|
1647
1649
|
if (!token) {
|
|
1648
|
-
return (0,
|
|
1650
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
|
|
1649
1651
|
}
|
|
1650
1652
|
const payload = verifyToken(token, config);
|
|
1651
1653
|
if (!payload || payload.type !== "refresh") {
|
|
1652
|
-
return (0,
|
|
1654
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
1653
1655
|
}
|
|
1654
1656
|
const valid = await sessions.exists(token);
|
|
1655
1657
|
if (!valid) {
|
|
1656
|
-
return (0,
|
|
1657
|
-
|
|
1658
|
+
return (0, import_core6.setApiResponse)(
|
|
1659
|
+
import_core6.HTTP.UNAUTHORIZED,
|
|
1658
1660
|
"TOKEN_REFRESH_FAILED",
|
|
1659
1661
|
"Session expired or already revoked"
|
|
1660
1662
|
);
|
|
1661
1663
|
}
|
|
1662
1664
|
const user = await users.findById(payload.sub);
|
|
1663
1665
|
if (!user || user.suspended || user.deletedAt) {
|
|
1664
|
-
return (0,
|
|
1666
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1665
1667
|
}
|
|
1666
1668
|
await sessions.delete(token);
|
|
1667
1669
|
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
@@ -1685,16 +1687,16 @@ function authController(store, config, bus) {
|
|
|
1685
1687
|
const body = ctx.meta["body"];
|
|
1686
1688
|
const rawEmail = body?.["email"];
|
|
1687
1689
|
if (typeof rawEmail !== "string") {
|
|
1688
|
-
return (0,
|
|
1690
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
1689
1691
|
}
|
|
1690
1692
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
1691
1693
|
if (!email2) {
|
|
1692
|
-
return (0,
|
|
1694
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1693
1695
|
}
|
|
1694
1696
|
const user = await users.findByEmail(email2);
|
|
1695
1697
|
if (!user) {
|
|
1696
|
-
return (0,
|
|
1697
|
-
|
|
1698
|
+
return (0, import_core6.setApiResponse)(
|
|
1699
|
+
import_core6.HTTP.OK,
|
|
1698
1700
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
1699
1701
|
"Password reset email sent (if account exists)."
|
|
1700
1702
|
);
|
|
@@ -1703,8 +1705,8 @@ function authController(store, config, bus) {
|
|
|
1703
1705
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1704
1706
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
1705
1707
|
if (remaining > 0) {
|
|
1706
|
-
return (0,
|
|
1707
|
-
|
|
1708
|
+
return (0, import_core6.setApiResponse)(
|
|
1709
|
+
import_core6.HTTP.OK,
|
|
1708
1710
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
1709
1711
|
"Password reset email sent (if account exists)."
|
|
1710
1712
|
);
|
|
@@ -1715,14 +1717,14 @@ function authController(store, config, bus) {
|
|
|
1715
1717
|
await passwordReset.create(user.id, pin, token, expiresAt);
|
|
1716
1718
|
const base = resolved.passwordResetUrl;
|
|
1717
1719
|
const resetUrl = base ? `${base}${base.includes("?") ? "&" : "?"}token=${token}` : "";
|
|
1718
|
-
await (0,
|
|
1720
|
+
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1719
1721
|
type: MESSAGE_KEYS.passwordReset,
|
|
1720
1722
|
locale: user.locale,
|
|
1721
1723
|
recipient: { email: email2, phone: null, deviceToken: null },
|
|
1722
1724
|
data: { pin, token, resetUrl }
|
|
1723
1725
|
}));
|
|
1724
|
-
return (0,
|
|
1725
|
-
|
|
1726
|
+
return (0, import_core6.setApiResponse)(
|
|
1727
|
+
import_core6.HTTP.OK,
|
|
1726
1728
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
1727
1729
|
"Password reset email sent (if account exists)."
|
|
1728
1730
|
);
|
|
@@ -1733,11 +1735,11 @@ function authController(store, config, bus) {
|
|
|
1733
1735
|
const password2 = body?.["password"];
|
|
1734
1736
|
const rawToken = body?.["token"];
|
|
1735
1737
|
if (typeof password2 !== "string") {
|
|
1736
|
-
return (0,
|
|
1738
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
|
|
1737
1739
|
}
|
|
1738
1740
|
if (password2.length < 8) {
|
|
1739
|
-
return (0,
|
|
1740
|
-
|
|
1741
|
+
return (0, import_core6.setApiResponse)(
|
|
1742
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1741
1743
|
"INVALID_PARAMETER",
|
|
1742
1744
|
"password must be at least 8 characters"
|
|
1743
1745
|
);
|
|
@@ -1747,8 +1749,8 @@ function authController(store, config, bus) {
|
|
|
1747
1749
|
row = await passwordReset.findByToken(rawToken.trim());
|
|
1748
1750
|
} else {
|
|
1749
1751
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
1750
|
-
return (0,
|
|
1751
|
-
|
|
1752
|
+
return (0, import_core6.setApiResponse)(
|
|
1753
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1752
1754
|
"INVALID_PARAMETER",
|
|
1753
1755
|
"a 6-digit pin or a reset token is required"
|
|
1754
1756
|
);
|
|
@@ -1756,8 +1758,8 @@ function authController(store, config, bus) {
|
|
|
1756
1758
|
row = await passwordReset.findByPin(raw.trim());
|
|
1757
1759
|
}
|
|
1758
1760
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1759
|
-
return (0,
|
|
1760
|
-
|
|
1761
|
+
return (0, import_core6.setApiResponse)(
|
|
1762
|
+
import_core6.HTTP.BAD_REQUEST,
|
|
1761
1763
|
"PASSWORD_RESET_FAILED",
|
|
1762
1764
|
"Invalid or expired reset credentials"
|
|
1763
1765
|
);
|
|
@@ -1775,14 +1777,14 @@ function authController(store, config, bus) {
|
|
|
1775
1777
|
tx.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [row.userId])
|
|
1776
1778
|
]);
|
|
1777
1779
|
});
|
|
1778
|
-
return (0,
|
|
1780
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
|
|
1779
1781
|
},
|
|
1780
1782
|
verify: async (ctx) => {
|
|
1781
1783
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
1782
|
-
return (0,
|
|
1784
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
1783
1785
|
}
|
|
1784
1786
|
if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
|
|
1785
|
-
return (0,
|
|
1787
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
1786
1788
|
verified: true,
|
|
1787
1789
|
email: ctx.user.email
|
|
1788
1790
|
});
|
|
@@ -1790,8 +1792,8 @@ function authController(store, config, bus) {
|
|
|
1790
1792
|
const body = ctx.meta["body"];
|
|
1791
1793
|
const raw = body?.["token"];
|
|
1792
1794
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
1793
|
-
return (0,
|
|
1794
|
-
|
|
1795
|
+
return (0, import_core6.setApiResponse)(
|
|
1796
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1795
1797
|
"INVALID_PARAMETER",
|
|
1796
1798
|
"token must be a 6-digit code"
|
|
1797
1799
|
);
|
|
@@ -1800,12 +1802,12 @@ function authController(store, config, bus) {
|
|
|
1800
1802
|
if (ctx.user.loginMethod === "phone") {
|
|
1801
1803
|
const record = await phoneVerif.findByUser(ctx.user.id, pin);
|
|
1802
1804
|
if (!record) {
|
|
1803
|
-
return (0,
|
|
1805
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
1804
1806
|
}
|
|
1805
1807
|
if (/* @__PURE__ */ new Date() > record.expiresAt) {
|
|
1806
1808
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
1807
|
-
return (0,
|
|
1808
|
-
|
|
1809
|
+
return (0, import_core6.setApiResponse)(
|
|
1810
|
+
import_core6.HTTP.BAD_REQUEST,
|
|
1809
1811
|
"VERIFICATION_FAILED",
|
|
1810
1812
|
"Verification code expired"
|
|
1811
1813
|
);
|
|
@@ -1818,11 +1820,11 @@ function authController(store, config, bus) {
|
|
|
1818
1820
|
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));
|
|
1819
1821
|
const verifiedUser = await users.findById(ctx.user.id);
|
|
1820
1822
|
if (!verifiedUser) {
|
|
1821
|
-
return (0,
|
|
1823
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
1822
1824
|
}
|
|
1823
1825
|
if (verifiedUser.suspended) {
|
|
1824
|
-
return (0,
|
|
1825
|
-
|
|
1826
|
+
return (0, import_core6.setApiResponse)(
|
|
1827
|
+
import_core6.HTTP.FORBIDDEN,
|
|
1826
1828
|
"ACCOUNT_SUSPENDED",
|
|
1827
1829
|
"Account suspended. Please contact support."
|
|
1828
1830
|
);
|
|
@@ -1844,10 +1846,10 @@ function authController(store, config, bus) {
|
|
|
1844
1846
|
}
|
|
1845
1847
|
const row = await emailVerif.findByUser(ctx.user.id, pin);
|
|
1846
1848
|
if (!row) {
|
|
1847
|
-
return (0,
|
|
1849
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
1848
1850
|
}
|
|
1849
1851
|
if (/* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1850
|
-
return (0,
|
|
1852
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
|
|
1851
1853
|
}
|
|
1852
1854
|
await store.transaction(async (tx) => {
|
|
1853
1855
|
await Promise.all([
|
|
@@ -1861,30 +1863,30 @@ function authController(store, config, bus) {
|
|
|
1861
1863
|
])
|
|
1862
1864
|
]);
|
|
1863
1865
|
});
|
|
1864
|
-
return (0,
|
|
1866
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
1865
1867
|
verified: true,
|
|
1866
1868
|
email: ctx.user.email
|
|
1867
1869
|
});
|
|
1868
1870
|
},
|
|
1869
1871
|
sendVerification: async (ctx) => {
|
|
1870
1872
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
1871
|
-
return (0,
|
|
1873
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
1872
1874
|
}
|
|
1873
1875
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
1874
1876
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1875
1877
|
if (ctx.user.loginMethod === "phone") {
|
|
1876
1878
|
const phone2 = ctx.user.phone;
|
|
1877
1879
|
if (!phone2) {
|
|
1878
|
-
return (0,
|
|
1879
|
-
|
|
1880
|
+
return (0, import_core6.setApiResponse)(
|
|
1881
|
+
import_core6.HTTP.BAD_REQUEST,
|
|
1880
1882
|
"NO_PHONE_ON_ACCOUNT",
|
|
1881
1883
|
"No phone number associated with this account"
|
|
1882
1884
|
);
|
|
1883
1885
|
}
|
|
1884
1886
|
const remaining2 = checkCooldown(await phoneVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
1885
1887
|
if (remaining2 > 0) {
|
|
1886
|
-
return (0,
|
|
1887
|
-
|
|
1888
|
+
return (0, import_core6.setApiResponse)(
|
|
1889
|
+
import_core6.HTTP.TOO_MANY_REQUESTS,
|
|
1888
1890
|
"VERIFICATION_COOLDOWN",
|
|
1889
1891
|
"Please wait before requesting a new code.",
|
|
1890
1892
|
{
|
|
@@ -1895,35 +1897,35 @@ function authController(store, config, bus) {
|
|
|
1895
1897
|
const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1896
1898
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
1897
1899
|
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
1898
|
-
await (0,
|
|
1900
|
+
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1899
1901
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1900
1902
|
locale: ctx.user.locale,
|
|
1901
1903
|
data: { otp },
|
|
1902
1904
|
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
1903
1905
|
}));
|
|
1904
|
-
return (0,
|
|
1905
|
-
|
|
1906
|
+
return (0, import_core6.setApiResponse)(
|
|
1907
|
+
import_core6.HTTP.OK,
|
|
1906
1908
|
"VERIFICATION_SENT",
|
|
1907
1909
|
"A verification code has been sent to your phone."
|
|
1908
1910
|
);
|
|
1909
1911
|
}
|
|
1910
1912
|
if (!ctx.user.email) {
|
|
1911
|
-
return (0,
|
|
1912
|
-
|
|
1913
|
+
return (0, import_core6.setApiResponse)(
|
|
1914
|
+
import_core6.HTTP.BAD_REQUEST,
|
|
1913
1915
|
"NO_EMAIL_ON_ACCOUNT",
|
|
1914
1916
|
"No email address associated with this account"
|
|
1915
1917
|
);
|
|
1916
1918
|
}
|
|
1917
1919
|
if (ctx.user.emailVerifiedAt) {
|
|
1918
|
-
return (0,
|
|
1920
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "EMAIL_VERIFIED", "Email already verified.", {
|
|
1919
1921
|
verified: true,
|
|
1920
1922
|
email: ctx.user.email
|
|
1921
1923
|
});
|
|
1922
1924
|
}
|
|
1923
1925
|
const remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
1924
1926
|
if (remaining > 0) {
|
|
1925
|
-
return (0,
|
|
1926
|
-
|
|
1927
|
+
return (0, import_core6.setApiResponse)(
|
|
1928
|
+
import_core6.HTTP.TOO_MANY_REQUESTS,
|
|
1927
1929
|
"VERIFICATION_COOLDOWN",
|
|
1928
1930
|
"Please wait before requesting a new code.",
|
|
1929
1931
|
{
|
|
@@ -1934,13 +1936,13 @@ function authController(store, config, bus) {
|
|
|
1934
1936
|
const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1935
1937
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1936
1938
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
1937
|
-
await (0,
|
|
1939
|
+
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1938
1940
|
type: MESSAGE_KEYS.emailVerification,
|
|
1939
1941
|
locale: ctx.user.locale,
|
|
1940
1942
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null },
|
|
1941
1943
|
data: { pin }
|
|
1942
1944
|
}));
|
|
1943
|
-
return (0,
|
|
1945
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
|
|
1944
1946
|
email: ctx.user.email
|
|
1945
1947
|
});
|
|
1946
1948
|
}
|
|
@@ -1949,11 +1951,11 @@ function authController(store, config, bus) {
|
|
|
1949
1951
|
|
|
1950
1952
|
// src/controllers/user.controller.ts
|
|
1951
1953
|
var import_node_crypto6 = require("crypto");
|
|
1952
|
-
var
|
|
1954
|
+
var import_core8 = require("@fonderie/core");
|
|
1953
1955
|
var import_events3 = require("@fonderie/events");
|
|
1954
1956
|
|
|
1955
1957
|
// src/dtos/login-activity.ts
|
|
1956
|
-
var
|
|
1958
|
+
var import_core7 = require("@fonderie/core");
|
|
1957
1959
|
function toLoginEventDTO(row) {
|
|
1958
1960
|
return {
|
|
1959
1961
|
id: row.id,
|
|
@@ -1967,10 +1969,10 @@ function toLoginEventDTO(row) {
|
|
|
1967
1969
|
}
|
|
1968
1970
|
function toLoginHistoryPageDTO(page) {
|
|
1969
1971
|
const last = page.events[page.events.length - 1];
|
|
1970
|
-
const nextCursor = page.hasMore && last ? (0,
|
|
1972
|
+
const nextCursor = page.hasMore && last ? (0, import_core7.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
|
|
1971
1973
|
return { events: page.events.map(toLoginEventDTO), nextCursor };
|
|
1972
1974
|
}
|
|
1973
|
-
var decodeLoginCursor =
|
|
1975
|
+
var decodeLoginCursor = import_core7.decodeKeysetCursor;
|
|
1974
1976
|
function toSessionDTO(row, currentSid) {
|
|
1975
1977
|
return {
|
|
1976
1978
|
id: row.id,
|
|
@@ -1999,9 +2001,9 @@ function userController(store, config, bus) {
|
|
|
1999
2001
|
me: async (ctx) => {
|
|
2000
2002
|
const user = await users.findById(ctx.user.id);
|
|
2001
2003
|
if (!user) {
|
|
2002
|
-
return (0,
|
|
2004
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2003
2005
|
}
|
|
2004
|
-
return (0,
|
|
2006
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
|
|
2005
2007
|
user: toUserDTO(user, ctx.user.phoneVerified)
|
|
2006
2008
|
});
|
|
2007
2009
|
},
|
|
@@ -2012,14 +2014,14 @@ function userController(store, config, bus) {
|
|
|
2012
2014
|
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
2013
2015
|
const outcomeParam = params.get("outcome");
|
|
2014
2016
|
if (outcomeParam !== null && outcomeParam !== "success" && outcomeParam !== "failed") {
|
|
2015
|
-
return (0,
|
|
2017
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
|
|
2016
2018
|
}
|
|
2017
2019
|
const cursorParam = params.get("cursor");
|
|
2018
2020
|
let cursor = null;
|
|
2019
2021
|
if (cursorParam) {
|
|
2020
2022
|
cursor = decodeLoginCursor(cursorParam);
|
|
2021
2023
|
if (!cursor) {
|
|
2022
|
-
return (0,
|
|
2024
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
2023
2025
|
}
|
|
2024
2026
|
}
|
|
2025
2027
|
const query = { userId: ctx.user.id, limit };
|
|
@@ -2030,8 +2032,8 @@ function userController(store, config, bus) {
|
|
|
2030
2032
|
if (from) query.from = new Date(from);
|
|
2031
2033
|
if (to) query.to = new Date(to);
|
|
2032
2034
|
const page = await loginEvents.listByUser(query);
|
|
2033
|
-
return (0,
|
|
2034
|
-
|
|
2035
|
+
return (0, import_core8.setApiResponse)(
|
|
2036
|
+
import_core8.HTTP.OK,
|
|
2035
2037
|
"LOGIN_HISTORY_FETCHED",
|
|
2036
2038
|
"Login history retrieved.",
|
|
2037
2039
|
toLoginHistoryPageDTO(page)
|
|
@@ -2041,32 +2043,32 @@ function userController(store, config, bus) {
|
|
|
2041
2043
|
listSessions: async (ctx) => {
|
|
2042
2044
|
const rows = await sessions.listLiveByUser(ctx.user.id);
|
|
2043
2045
|
const currentSid = ctx.user.sid ?? null;
|
|
2044
|
-
return (0,
|
|
2046
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
|
|
2045
2047
|
sessions: rows.map((r) => toSessionDTO(r, currentSid))
|
|
2046
2048
|
});
|
|
2047
2049
|
},
|
|
2048
2050
|
terminateSession: async (ctx) => {
|
|
2049
2051
|
const id = ctx.meta["params"]?.["id"];
|
|
2050
2052
|
if (!id) {
|
|
2051
|
-
return (0,
|
|
2053
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2052
2054
|
}
|
|
2053
2055
|
const removed = await sessions.terminateById(ctx.user.id, id);
|
|
2054
2056
|
if (!removed) {
|
|
2055
|
-
return (0,
|
|
2057
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2056
2058
|
}
|
|
2057
|
-
return (0,
|
|
2059
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSION_TERMINATED", "Session terminated.", { id });
|
|
2058
2060
|
},
|
|
2059
2061
|
terminateOtherSessions: async (ctx) => {
|
|
2060
2062
|
const currentSid = ctx.user.sid ?? null;
|
|
2061
2063
|
if (!currentSid) {
|
|
2062
|
-
return (0,
|
|
2063
|
-
|
|
2064
|
+
return (0, import_core8.setApiResponse)(
|
|
2065
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2064
2066
|
"NO_CURRENT_SESSION",
|
|
2065
2067
|
"Cannot terminate others without a current session context"
|
|
2066
2068
|
);
|
|
2067
2069
|
}
|
|
2068
2070
|
const count = await sessions.terminateOthers(ctx.user.id, currentSid);
|
|
2069
|
-
return (0,
|
|
2071
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
|
|
2070
2072
|
},
|
|
2071
2073
|
updateProfile: async (ctx) => {
|
|
2072
2074
|
const body = ctx.meta["body"];
|
|
@@ -2076,18 +2078,18 @@ function userController(store, config, bus) {
|
|
|
2076
2078
|
if (body?.[key] !== void 0) fields[key] = body[key];
|
|
2077
2079
|
}
|
|
2078
2080
|
if (Object.keys(fields).length === 0) {
|
|
2079
|
-
return (0,
|
|
2080
|
-
|
|
2081
|
+
return (0, import_core8.setApiResponse)(
|
|
2082
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2081
2083
|
"INVALID_PARAMETER",
|
|
2082
2084
|
"Provide at least one of: firstName, lastName, avatarUrl"
|
|
2083
2085
|
);
|
|
2084
2086
|
}
|
|
2085
2087
|
const row = await users.update(ctx.user.id, fields);
|
|
2086
2088
|
if (!row) {
|
|
2087
|
-
return (0,
|
|
2089
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2088
2090
|
}
|
|
2089
2091
|
const updated = await users.findById(ctx.user.id);
|
|
2090
|
-
return (0,
|
|
2092
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PROFILE_UPDATED", "Profile updated.", {
|
|
2091
2093
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2092
2094
|
});
|
|
2093
2095
|
},
|
|
@@ -2104,14 +2106,14 @@ function userController(store, config, bus) {
|
|
|
2104
2106
|
if (Object.keys(patch).length > 0) fields.patch = patch;
|
|
2105
2107
|
const row = await users.updatePreferences(ctx.user.id, fields);
|
|
2106
2108
|
if (!row) {
|
|
2107
|
-
return (0,
|
|
2108
|
-
|
|
2109
|
+
return (0, import_core8.setApiResponse)(
|
|
2110
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2109
2111
|
"INVALID_PARAMETER",
|
|
2110
2112
|
"Provide at least one preference field"
|
|
2111
2113
|
);
|
|
2112
2114
|
}
|
|
2113
2115
|
const updated = await users.findById(ctx.user.id);
|
|
2114
|
-
return (0,
|
|
2116
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
|
|
2115
2117
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2116
2118
|
});
|
|
2117
2119
|
},
|
|
@@ -2120,44 +2122,44 @@ function userController(store, config, bus) {
|
|
|
2120
2122
|
const newEmail = body?.["email"];
|
|
2121
2123
|
const normalised = typeof newEmail === "string" ? normalizeEmailSafe(newEmail) : null;
|
|
2122
2124
|
if (!normalised) {
|
|
2123
|
-
return (0,
|
|
2124
|
-
|
|
2125
|
+
return (0, import_core8.setApiResponse)(
|
|
2126
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2125
2127
|
"INVALID_PARAMETER",
|
|
2126
2128
|
"A valid email address is required"
|
|
2127
2129
|
);
|
|
2128
2130
|
}
|
|
2129
2131
|
const oldEmail = ctx.user.email;
|
|
2130
2132
|
if (normalised === oldEmail) {
|
|
2131
|
-
return (0,
|
|
2132
|
-
|
|
2133
|
+
return (0, import_core8.setApiResponse)(
|
|
2134
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2133
2135
|
"INVALID_PARAMETER",
|
|
2134
2136
|
"New email must differ from current email"
|
|
2135
2137
|
);
|
|
2136
2138
|
}
|
|
2137
2139
|
const existing = await users.findByEmail(normalised);
|
|
2138
2140
|
if (existing) {
|
|
2139
|
-
return (0,
|
|
2141
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
2140
2142
|
}
|
|
2141
2143
|
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2142
2144
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2143
2145
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2144
2146
|
await users.updateEmail(ctx.user.id, normalised);
|
|
2145
|
-
await (0,
|
|
2147
|
+
await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2146
2148
|
type: MESSAGE_KEYS.emailVerification,
|
|
2147
2149
|
locale: ctx.user.locale,
|
|
2148
2150
|
data: { pin },
|
|
2149
2151
|
recipient: { email: normalised, phone: null, deviceToken: null }
|
|
2150
2152
|
}));
|
|
2151
2153
|
if (oldEmail) {
|
|
2152
|
-
await (0,
|
|
2154
|
+
await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2153
2155
|
type: MESSAGE_KEYS.emailChanged,
|
|
2154
2156
|
locale: ctx.user.locale,
|
|
2155
2157
|
data: { newEmail: normalised },
|
|
2156
2158
|
recipient: { email: oldEmail, phone: null, deviceToken: null }
|
|
2157
2159
|
}));
|
|
2158
2160
|
}
|
|
2159
|
-
return (0,
|
|
2160
|
-
|
|
2161
|
+
return (0, import_core8.setApiResponse)(
|
|
2162
|
+
import_core8.HTTP.OK,
|
|
2161
2163
|
"EMAIL_UPDATED",
|
|
2162
2164
|
"Email updated. A verification code has been sent to your new address.",
|
|
2163
2165
|
{
|
|
@@ -2169,8 +2171,8 @@ function userController(store, config, bus) {
|
|
|
2169
2171
|
const body = ctx.meta["body"];
|
|
2170
2172
|
const newPhone = body?.["phone"];
|
|
2171
2173
|
if (!isValidPhone2(newPhone)) {
|
|
2172
|
-
return (0,
|
|
2173
|
-
|
|
2174
|
+
return (0, import_core8.setApiResponse)(
|
|
2175
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2174
2176
|
"INVALID_PARAMETER",
|
|
2175
2177
|
"A valid phone number is required"
|
|
2176
2178
|
);
|
|
@@ -2178,28 +2180,28 @@ function userController(store, config, bus) {
|
|
|
2178
2180
|
const normalised = normalizePhone2(newPhone);
|
|
2179
2181
|
const existing = await users.findByPhone(normalised);
|
|
2180
2182
|
if (existing) {
|
|
2181
|
-
return (0,
|
|
2183
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
2182
2184
|
}
|
|
2183
2185
|
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2184
2186
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
2185
2187
|
await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
|
|
2186
2188
|
await users.updatePhone(ctx.user.id, normalised);
|
|
2187
|
-
await (0,
|
|
2189
|
+
await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2188
2190
|
type: MESSAGE_KEYS.phoneOtp,
|
|
2189
2191
|
locale: ctx.user.locale,
|
|
2190
2192
|
data: { otp },
|
|
2191
2193
|
recipient: { email: null, phone: normalised, deviceToken: null }
|
|
2192
2194
|
}));
|
|
2193
2195
|
if (ctx.user.email) {
|
|
2194
|
-
await (0,
|
|
2196
|
+
await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2195
2197
|
type: MESSAGE_KEYS.phoneChanged,
|
|
2196
2198
|
locale: ctx.user.locale,
|
|
2197
2199
|
data: {},
|
|
2198
2200
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
2199
2201
|
}));
|
|
2200
2202
|
}
|
|
2201
|
-
return (0,
|
|
2202
|
-
|
|
2203
|
+
return (0, import_core8.setApiResponse)(
|
|
2204
|
+
import_core8.HTTP.OK,
|
|
2203
2205
|
"PHONE_UPDATED",
|
|
2204
2206
|
"Phone number updated. A verification code has been sent to your new number.",
|
|
2205
2207
|
{
|
|
@@ -2212,28 +2214,28 @@ function userController(store, config, bus) {
|
|
|
2212
2214
|
const currentPassword = body?.["currentPassword"];
|
|
2213
2215
|
const newPassword = body?.["newPassword"];
|
|
2214
2216
|
if (typeof currentPassword !== "string" || !currentPassword) {
|
|
2215
|
-
return (0,
|
|
2217
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
|
|
2216
2218
|
}
|
|
2217
2219
|
if (typeof newPassword !== "string" || newPassword.length < 8) {
|
|
2218
|
-
return (0,
|
|
2220
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
|
|
2219
2221
|
}
|
|
2220
2222
|
const user = await users.findById(ctx.user.id);
|
|
2221
|
-
if (!user) return (0,
|
|
2223
|
+
if (!user) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2222
2224
|
const { hashPassword: hashPassword2, verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password(), password_exports));
|
|
2223
2225
|
const valid = await verifyPassword2(currentPassword, user.passwordHash ?? "");
|
|
2224
2226
|
if (!valid) {
|
|
2225
|
-
return (0,
|
|
2227
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
|
|
2226
2228
|
}
|
|
2227
2229
|
const hash = await hashPassword2(newPassword);
|
|
2228
2230
|
await users.updatePassword(ctx.user.id, hash);
|
|
2229
2231
|
await sessions.deleteByUser(ctx.user.id);
|
|
2230
|
-
return (0,
|
|
2232
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PASSWORD_CHANGED", "Password updated successfully.");
|
|
2231
2233
|
},
|
|
2232
2234
|
deleteMe: async (ctx) => {
|
|
2233
2235
|
const userId = ctx.user.id;
|
|
2234
2236
|
await users.softDelete(userId);
|
|
2235
2237
|
const reqId = ctx.meta["requestId"];
|
|
2236
|
-
await (0,
|
|
2238
|
+
await (0, import_core8.background)(bus?.emit(
|
|
2237
2239
|
EVENT_KEYS.userDeleted,
|
|
2238
2240
|
{ userId },
|
|
2239
2241
|
reqId !== void 0 ? { requestId: reqId } : void 0
|
|
@@ -2251,7 +2253,7 @@ function userController(store, config, bus) {
|
|
|
2251
2253
|
// no MFA secret, no session tokens (session metadata only).
|
|
2252
2254
|
exportMe: async (ctx) => {
|
|
2253
2255
|
const user = await users.findById(ctx.user.id);
|
|
2254
|
-
if (!user) return (0,
|
|
2256
|
+
if (!user) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2255
2257
|
const sessionRows = await sessions.listByUser(ctx.user.id);
|
|
2256
2258
|
const bundle = {
|
|
2257
2259
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2260,8 +2262,8 @@ function userController(store, config, bus) {
|
|
|
2260
2262
|
id: s.id,
|
|
2261
2263
|
userAgent: s.userAgent,
|
|
2262
2264
|
ipAddress: s.ipAddress,
|
|
2263
|
-
createdAt: (0,
|
|
2264
|
-
expiresAt: (0,
|
|
2265
|
+
createdAt: (0, import_core8.dateOrEmpty)(s.createdAt),
|
|
2266
|
+
expiresAt: (0, import_core8.dateOrEmpty)(s.expiresAt)
|
|
2265
2267
|
})),
|
|
2266
2268
|
security: {
|
|
2267
2269
|
mfaEnabled: user.mfaEnabled === true,
|
|
@@ -2280,7 +2282,7 @@ function userController(store, config, bus) {
|
|
|
2280
2282
|
}
|
|
2281
2283
|
bundle["modules"] = modules;
|
|
2282
2284
|
}
|
|
2283
|
-
return (0,
|
|
2285
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "DATA_EXPORT", "Your account data.", bundle);
|
|
2284
2286
|
}
|
|
2285
2287
|
};
|
|
2286
2288
|
}
|
|
@@ -2288,7 +2290,7 @@ function userController(store, config, bus) {
|
|
|
2288
2290
|
// src/controllers/oauth.controller.ts
|
|
2289
2291
|
var import_node_crypto7 = require("crypto");
|
|
2290
2292
|
var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
|
|
2291
|
-
var
|
|
2293
|
+
var import_core9 = require("@fonderie/core");
|
|
2292
2294
|
|
|
2293
2295
|
// src/models/consumed-token.model.ts
|
|
2294
2296
|
var ConsumedTokenModel = class {
|
|
@@ -2397,18 +2399,18 @@ function oauthController(store, config) {
|
|
|
2397
2399
|
const completeAppleLogin = async (ctx, claims) => {
|
|
2398
2400
|
const meta = requestMeta(ctx);
|
|
2399
2401
|
if (!claims.email) {
|
|
2400
|
-
return (0,
|
|
2402
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "No email in Apple identity token");
|
|
2401
2403
|
}
|
|
2402
2404
|
if (!claims.emailVerified) {
|
|
2403
|
-
return (0,
|
|
2405
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Apple account email is not verified");
|
|
2404
2406
|
}
|
|
2405
2407
|
const normalizedEmail = normalizeEmailSafe(claims.email);
|
|
2406
2408
|
if (!normalizedEmail) {
|
|
2407
|
-
return (0,
|
|
2409
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid email in Apple identity token");
|
|
2408
2410
|
}
|
|
2409
2411
|
const upserted = await users.upsertByProvider(normalizedEmail, "apple", claims.sub ?? "");
|
|
2410
2412
|
if (!upserted) {
|
|
2411
|
-
loginEvents.recordSafe({
|
|
2413
|
+
await loginEvents.recordSafe({
|
|
2412
2414
|
userId: null,
|
|
2413
2415
|
emailAttempted: normalizedEmail,
|
|
2414
2416
|
method: "oauth-apple",
|
|
@@ -2416,15 +2418,15 @@ function oauthController(store, config) {
|
|
|
2416
2418
|
failureReason: "provider_upsert_failed",
|
|
2417
2419
|
...meta
|
|
2418
2420
|
});
|
|
2419
|
-
return (0,
|
|
2421
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
|
|
2420
2422
|
}
|
|
2421
2423
|
const fullUser = await users.findById(upserted.id);
|
|
2422
2424
|
if (!fullUser) {
|
|
2423
|
-
return (0,
|
|
2425
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
|
|
2424
2426
|
}
|
|
2425
2427
|
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, { loginMethod: "apple" });
|
|
2426
2428
|
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
2427
|
-
loginEvents.recordSafe({
|
|
2429
|
+
await loginEvents.recordSafe({
|
|
2428
2430
|
userId: upserted.id,
|
|
2429
2431
|
emailAttempted: normalizedEmail,
|
|
2430
2432
|
method: "oauth-apple",
|
|
@@ -2454,8 +2456,8 @@ function oauthController(store, config) {
|
|
|
2454
2456
|
googleInit: async (_ctx) => {
|
|
2455
2457
|
const google = config.google;
|
|
2456
2458
|
if (!google) {
|
|
2457
|
-
return (0,
|
|
2458
|
-
|
|
2459
|
+
return (0, import_core9.setApiResponse)(
|
|
2460
|
+
import_core9.HTTP.NOT_IMPLEMENTED,
|
|
2459
2461
|
"NOT_CONFIGURED",
|
|
2460
2462
|
"Google OAuth not configured"
|
|
2461
2463
|
);
|
|
@@ -2489,8 +2491,8 @@ function oauthController(store, config) {
|
|
|
2489
2491
|
googleCallback: async (ctx) => {
|
|
2490
2492
|
const google = config.google;
|
|
2491
2493
|
if (!google) {
|
|
2492
|
-
return (0,
|
|
2493
|
-
|
|
2494
|
+
return (0, import_core9.setApiResponse)(
|
|
2495
|
+
import_core9.HTTP.NOT_IMPLEMENTED,
|
|
2494
2496
|
"NOT_CONFIGURED",
|
|
2495
2497
|
"Google OAuth not configured"
|
|
2496
2498
|
);
|
|
@@ -2498,14 +2500,14 @@ function oauthController(store, config) {
|
|
|
2498
2500
|
const url = new URL(ctx.request.url);
|
|
2499
2501
|
const code = url.searchParams.get("code");
|
|
2500
2502
|
if (!code) {
|
|
2501
|
-
return (0,
|
|
2503
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
2502
2504
|
}
|
|
2503
2505
|
const returnedState = url.searchParams.get("state") ?? "";
|
|
2504
2506
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
2505
2507
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
2506
|
-
if (!returnedState || !expectedState || !(0,
|
|
2507
|
-
return (0,
|
|
2508
|
-
|
|
2508
|
+
if (!returnedState || !expectedState || !(0, import_core9.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
|
|
2509
|
+
return (0, import_core9.setApiResponse)(
|
|
2510
|
+
import_core9.HTTP.BAD_REQUEST,
|
|
2509
2511
|
"GOOGLE_AUTH_FAILED",
|
|
2510
2512
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
2511
2513
|
);
|
|
@@ -2523,8 +2525,8 @@ function oauthController(store, config) {
|
|
|
2523
2525
|
});
|
|
2524
2526
|
const tokenData = await tokenRes.json();
|
|
2525
2527
|
if (!tokenData.id_token) {
|
|
2526
|
-
return (0,
|
|
2527
|
-
|
|
2528
|
+
return (0, import_core9.setApiResponse)(
|
|
2529
|
+
import_core9.HTTP.BAD_REQUEST,
|
|
2528
2530
|
"GOOGLE_AUTH_FAILED",
|
|
2529
2531
|
"OAuth token exchange failed"
|
|
2530
2532
|
);
|
|
@@ -2533,32 +2535,32 @@ function oauthController(store, config) {
|
|
|
2533
2535
|
Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
|
|
2534
2536
|
);
|
|
2535
2537
|
if (payload.aud !== google.clientId) {
|
|
2536
|
-
return (0,
|
|
2538
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
|
|
2537
2539
|
}
|
|
2538
2540
|
if (payload.iss !== "https://accounts.google.com" && payload.iss !== "accounts.google.com") {
|
|
2539
|
-
return (0,
|
|
2541
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
|
|
2540
2542
|
}
|
|
2541
2543
|
if (typeof payload.exp !== "number" || payload.exp * 1e3 < Date.now()) {
|
|
2542
|
-
return (0,
|
|
2544
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
|
|
2543
2545
|
}
|
|
2544
2546
|
if (!payload.email) {
|
|
2545
|
-
return (0,
|
|
2547
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
|
|
2546
2548
|
}
|
|
2547
2549
|
if (payload.email_verified !== true) {
|
|
2548
|
-
return (0,
|
|
2549
|
-
|
|
2550
|
+
return (0, import_core9.setApiResponse)(
|
|
2551
|
+
import_core9.HTTP.BAD_REQUEST,
|
|
2550
2552
|
"GOOGLE_AUTH_FAILED",
|
|
2551
2553
|
"Google account email is not verified"
|
|
2552
2554
|
);
|
|
2553
2555
|
}
|
|
2554
2556
|
const normalizedEmail = normalizeEmailSafe(payload.email);
|
|
2555
2557
|
if (!normalizedEmail) {
|
|
2556
|
-
return (0,
|
|
2558
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "Invalid email in OAuth response");
|
|
2557
2559
|
}
|
|
2558
2560
|
const meta = requestMeta(ctx);
|
|
2559
2561
|
const upserted = await users.upsertByProvider(normalizedEmail, "google", payload.sub ?? "");
|
|
2560
2562
|
if (!upserted) {
|
|
2561
|
-
loginEvents.recordSafe({
|
|
2563
|
+
await loginEvents.recordSafe({
|
|
2562
2564
|
userId: null,
|
|
2563
2565
|
emailAttempted: normalizedEmail,
|
|
2564
2566
|
method: "oauth-google",
|
|
@@ -2566,17 +2568,17 @@ function oauthController(store, config) {
|
|
|
2566
2568
|
failureReason: "provider_upsert_failed",
|
|
2567
2569
|
...meta
|
|
2568
2570
|
});
|
|
2569
|
-
return (0,
|
|
2571
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
2570
2572
|
}
|
|
2571
2573
|
const fullUser = await users.findById(upserted.id);
|
|
2572
2574
|
if (!fullUser) {
|
|
2573
|
-
return (0,
|
|
2575
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
2574
2576
|
}
|
|
2575
2577
|
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {
|
|
2576
2578
|
loginMethod: "google"
|
|
2577
2579
|
});
|
|
2578
2580
|
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
2579
|
-
loginEvents.recordSafe({
|
|
2581
|
+
await loginEvents.recordSafe({
|
|
2580
2582
|
userId: upserted.id,
|
|
2581
2583
|
emailAttempted: normalizedEmail,
|
|
2582
2584
|
method: "oauth-google",
|
|
@@ -2605,7 +2607,7 @@ function oauthController(store, config) {
|
|
|
2605
2607
|
appleInit: async (_ctx) => {
|
|
2606
2608
|
const apple = config.apple;
|
|
2607
2609
|
if (!apple) {
|
|
2608
|
-
return (0,
|
|
2610
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
2609
2611
|
}
|
|
2610
2612
|
const state = (0, import_node_crypto7.randomBytes)(16).toString("hex");
|
|
2611
2613
|
const params = new URLSearchParams({
|
|
@@ -2636,20 +2638,20 @@ function oauthController(store, config) {
|
|
|
2636
2638
|
appleCallback: async (ctx) => {
|
|
2637
2639
|
const apple = config.apple;
|
|
2638
2640
|
if (!apple) {
|
|
2639
|
-
return (0,
|
|
2641
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
2640
2642
|
}
|
|
2641
2643
|
const pre = ctx.meta["body"];
|
|
2642
2644
|
const form = pre && typeof pre === "object" ? new URLSearchParams(pre) : new URLSearchParams(await ctx.request.text());
|
|
2643
2645
|
const code = form.get("code");
|
|
2644
2646
|
if (!code) {
|
|
2645
|
-
return (0,
|
|
2647
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
2646
2648
|
}
|
|
2647
2649
|
const returnedState = form.get("state") ?? "";
|
|
2648
2650
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
2649
2651
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
2650
|
-
if (!returnedState || !expectedState || !(0,
|
|
2651
|
-
return (0,
|
|
2652
|
-
|
|
2652
|
+
if (!returnedState || !expectedState || !(0, import_core9.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
|
|
2653
|
+
return (0, import_core9.setApiResponse)(
|
|
2654
|
+
import_core9.HTTP.BAD_REQUEST,
|
|
2653
2655
|
"APPLE_AUTH_FAILED",
|
|
2654
2656
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
2655
2657
|
);
|
|
@@ -2667,11 +2669,11 @@ function oauthController(store, config) {
|
|
|
2667
2669
|
});
|
|
2668
2670
|
const tokenData = await tokenRes.json();
|
|
2669
2671
|
if (!tokenData.id_token) {
|
|
2670
|
-
return (0,
|
|
2672
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
|
|
2671
2673
|
}
|
|
2672
2674
|
const claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });
|
|
2673
2675
|
if (!claims) {
|
|
2674
|
-
return (0,
|
|
2676
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
2675
2677
|
}
|
|
2676
2678
|
return completeAppleLogin(ctx, {
|
|
2677
2679
|
email: claims.email,
|
|
@@ -2682,18 +2684,18 @@ function oauthController(store, config) {
|
|
|
2682
2684
|
appleNative: async (ctx) => {
|
|
2683
2685
|
const apple = config.apple;
|
|
2684
2686
|
if (!apple) {
|
|
2685
|
-
return (0,
|
|
2687
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
2686
2688
|
}
|
|
2687
2689
|
const body = ctx.meta["body"];
|
|
2688
2690
|
const identityToken = body?.identityToken;
|
|
2689
2691
|
if (!identityToken) {
|
|
2690
|
-
return (0,
|
|
2692
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
|
|
2691
2693
|
}
|
|
2692
2694
|
const audiences = apple.nativeClientIds && apple.nativeClientIds.length ? apple.nativeClientIds : [apple.clientId];
|
|
2693
2695
|
const claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });
|
|
2694
2696
|
if (!claims) {
|
|
2695
2697
|
const meta = requestMeta(ctx);
|
|
2696
|
-
loginEvents.recordSafe({
|
|
2698
|
+
await loginEvents.recordSafe({
|
|
2697
2699
|
userId: null,
|
|
2698
2700
|
emailAttempted: null,
|
|
2699
2701
|
method: "oauth-apple",
|
|
@@ -2701,14 +2703,14 @@ function oauthController(store, config) {
|
|
|
2701
2703
|
failureReason: "invalid_identity_token",
|
|
2702
2704
|
...meta
|
|
2703
2705
|
});
|
|
2704
|
-
return (0,
|
|
2706
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
2705
2707
|
}
|
|
2706
2708
|
const tokenHash = (0, import_node_crypto7.createHash)("sha256").update(identityToken).digest("hex");
|
|
2707
2709
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
2708
2710
|
const firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));
|
|
2709
2711
|
if (!firstUse) {
|
|
2710
2712
|
const meta = requestMeta(ctx);
|
|
2711
|
-
loginEvents.recordSafe({
|
|
2713
|
+
await loginEvents.recordSafe({
|
|
2712
2714
|
userId: null,
|
|
2713
2715
|
emailAttempted: claims.email ?? null,
|
|
2714
2716
|
method: "oauth-apple",
|
|
@@ -2716,8 +2718,8 @@ function oauthController(store, config) {
|
|
|
2716
2718
|
failureReason: "token_replayed",
|
|
2717
2719
|
...meta
|
|
2718
2720
|
});
|
|
2719
|
-
return (0,
|
|
2720
|
-
|
|
2721
|
+
return (0, import_core9.setApiResponse)(
|
|
2722
|
+
import_core9.HTTP.UNAUTHORIZED,
|
|
2721
2723
|
"APPLE_AUTH_FAILED",
|
|
2722
2724
|
"This Apple identity token has already been used"
|
|
2723
2725
|
);
|
|
@@ -2815,17 +2817,17 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
2815
2817
|
}
|
|
2816
2818
|
|
|
2817
2819
|
// src/services/config-guard.ts
|
|
2818
|
-
var
|
|
2820
|
+
var import_core10 = require("@fonderie/core");
|
|
2819
2821
|
var MODULE = "@fonderie/auth";
|
|
2820
2822
|
function collectAuthConfigProblems(config) {
|
|
2821
2823
|
const problems = [];
|
|
2822
2824
|
const secret = config.jwtSecret ?? "";
|
|
2823
|
-
const secretProblem = (0,
|
|
2825
|
+
const secretProblem = (0, import_core10.secretStrengthProblem)(secret);
|
|
2824
2826
|
if (secretProblem === "too-short") {
|
|
2825
2827
|
problems.push({
|
|
2826
2828
|
module: MODULE,
|
|
2827
2829
|
severity: "error",
|
|
2828
|
-
message: `jwtSecret must be at least ${
|
|
2830
|
+
message: `jwtSecret must be at least ${import_core10.MIN_SECRET_LENGTH} characters (got ${secret.length})`
|
|
2829
2831
|
});
|
|
2830
2832
|
} else if (secretProblem === "placeholder") {
|
|
2831
2833
|
problems.push({
|
|
@@ -2849,7 +2851,7 @@ function collectAuthConfigProblems(config) {
|
|
|
2849
2851
|
severity: "error",
|
|
2850
2852
|
message: "google OAuth is configured but clientId, clientSecret, or redirectUri is missing"
|
|
2851
2853
|
});
|
|
2852
|
-
} else if (
|
|
2854
|
+
} else if (import_core10.PLACEHOLDER_SECRET.test(clientSecret)) {
|
|
2853
2855
|
problems.push({
|
|
2854
2856
|
module: MODULE,
|
|
2855
2857
|
severity: "error",
|