@fonderie/auth 7.3.3 → 7.3.5
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 +232 -246
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +56 -70
- 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,35 +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
|
-
bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
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
|
-
})
|
|
1019
|
-
|
|
1020
|
-
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "MFA_VERIFIED", "MFA verified successfully.", {
|
|
1020
|
+
}));
|
|
1021
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "MFA_VERIFIED", "MFA verified successfully.", {
|
|
1021
1022
|
mfaEnabled: true
|
|
1022
1023
|
});
|
|
1023
1024
|
} else if (/^[A-Z0-9]{8}$/i.test(token)) {
|
|
1024
1025
|
if (!ctx.user.mfaPending) {
|
|
1025
|
-
return (0,
|
|
1026
|
-
|
|
1026
|
+
return (0, import_core5.setApiResponse)(
|
|
1027
|
+
import_core5.HTTP.FORBIDDEN,
|
|
1027
1028
|
"MFA_NOT_PENDING",
|
|
1028
1029
|
"Use the mfaToken from the login response"
|
|
1029
1030
|
);
|
|
1030
1031
|
}
|
|
1031
1032
|
if (!ctx.user.mfaEnabled) {
|
|
1032
|
-
return (0,
|
|
1033
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
|
|
1033
1034
|
}
|
|
1034
1035
|
const unused = await backupCodes.findUnused(ctx.user.id);
|
|
1035
1036
|
const checks = await Promise.all(
|
|
@@ -1040,26 +1041,26 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1040
1041
|
);
|
|
1041
1042
|
const matched = checks.find((r) => r.match);
|
|
1042
1043
|
if (!matched) {
|
|
1043
|
-
if (isLoginCompletion) failedMfa();
|
|
1044
|
-
return (0,
|
|
1044
|
+
if (isLoginCompletion) await failedMfa();
|
|
1045
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid backup code");
|
|
1045
1046
|
}
|
|
1046
1047
|
await backupCodes.consume(matched.id);
|
|
1047
1048
|
} else {
|
|
1048
1049
|
if (!ctx.user.mfaPending) {
|
|
1049
|
-
return (0,
|
|
1050
|
-
|
|
1050
|
+
return (0, import_core5.setApiResponse)(
|
|
1051
|
+
import_core5.HTTP.FORBIDDEN,
|
|
1051
1052
|
"MFA_NOT_PENDING",
|
|
1052
1053
|
"Use the mfaToken from the login response"
|
|
1053
1054
|
);
|
|
1054
1055
|
}
|
|
1055
1056
|
const storedSecret = await users.getMfaSecret(ctx.user.id);
|
|
1056
1057
|
if (!storedSecret) {
|
|
1057
|
-
return (0,
|
|
1058
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
|
|
1058
1059
|
}
|
|
1059
1060
|
const secret = mfaCipher.decrypt(storedSecret);
|
|
1060
1061
|
if (!verifyTotpToken(token, secret)) {
|
|
1061
|
-
if (isLoginCompletion) failedMfa();
|
|
1062
|
-
return (0,
|
|
1062
|
+
if (isLoginCompletion) await failedMfa();
|
|
1063
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid MFA token");
|
|
1063
1064
|
}
|
|
1064
1065
|
if (!ctx.user.mfaEnabled) {
|
|
1065
1066
|
await users.enableMfa(ctx.user.id);
|
|
@@ -1071,7 +1072,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1071
1072
|
});
|
|
1072
1073
|
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
1073
1074
|
if (isLoginCompletion) {
|
|
1074
|
-
loginEvents.recordSafe({
|
|
1075
|
+
await loginEvents.recordSafe({
|
|
1075
1076
|
userId: ctx.user.id,
|
|
1076
1077
|
emailAttempted: ctx.user.email,
|
|
1077
1078
|
method: "mfa",
|
|
@@ -1081,7 +1082,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1081
1082
|
}
|
|
1082
1083
|
const fullUser = await users.findById(ctx.user.id);
|
|
1083
1084
|
if (!fullUser) {
|
|
1084
|
-
return (0,
|
|
1085
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "User not found after MFA verify");
|
|
1085
1086
|
}
|
|
1086
1087
|
return Response.json(
|
|
1087
1088
|
{
|
|
@@ -1103,27 +1104,26 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1103
1104
|
const body = ctx.meta["body"];
|
|
1104
1105
|
const token = body?.["token"];
|
|
1105
1106
|
if (typeof token !== "string") {
|
|
1106
|
-
return (0,
|
|
1107
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
|
|
1107
1108
|
}
|
|
1108
1109
|
if (!ctx.user.mfaEnabled) {
|
|
1109
|
-
return (0,
|
|
1110
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
|
|
1110
1111
|
}
|
|
1111
1112
|
const storedSecret = await users.getMfaSecret(ctx.user.id);
|
|
1112
1113
|
const secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;
|
|
1113
1114
|
if (!secret || !verifyTotpToken(token, secret)) {
|
|
1114
|
-
return (0,
|
|
1115
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
|
|
1115
1116
|
}
|
|
1116
1117
|
const plainCodes = generateBackupCodes();
|
|
1117
1118
|
const codeHashes = await Promise.all(plainCodes.map((c) => hashPassword(c)));
|
|
1118
1119
|
await backupCodes.replace(ctx.user.id, codeHashes);
|
|
1119
|
-
bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
1120
|
+
await (0, import_core5.background)(bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
1120
1121
|
type: MESSAGE_KEYS.mfaBackupCodesRegenerated,
|
|
1121
1122
|
locale: ctx.user.locale,
|
|
1122
1123
|
data: {},
|
|
1123
1124
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
1124
|
-
})
|
|
1125
|
-
|
|
1126
|
-
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "BACKUP_CODES_REGENERATED", "Backup codes regenerated.", {
|
|
1125
|
+
}));
|
|
1126
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "BACKUP_CODES_REGENERATED", "Backup codes regenerated.", {
|
|
1127
1127
|
backupCodes: plainCodes
|
|
1128
1128
|
});
|
|
1129
1129
|
},
|
|
@@ -1132,26 +1132,25 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1132
1132
|
const body = ctx.meta["body"];
|
|
1133
1133
|
const token = body?.["token"];
|
|
1134
1134
|
if (typeof token !== "string") {
|
|
1135
|
-
return (0,
|
|
1135
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
|
|
1136
1136
|
}
|
|
1137
1137
|
const user = await users.findById(ctx.user.id);
|
|
1138
1138
|
if (!user || !user.mfaEnabled) {
|
|
1139
|
-
return (0,
|
|
1139
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
|
|
1140
1140
|
}
|
|
1141
1141
|
const storedSecret = await users.getMfaSecret(ctx.user.id);
|
|
1142
1142
|
const secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;
|
|
1143
1143
|
if (!secret || !verifyTotpToken(token, secret)) {
|
|
1144
|
-
return (0,
|
|
1144
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
|
|
1145
1145
|
}
|
|
1146
1146
|
await Promise.all([users.disableMfa(ctx.user.id), backupCodes.deleteByUser(ctx.user.id)]);
|
|
1147
|
-
bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
1147
|
+
await (0, import_core5.background)(bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
1148
1148
|
type: MESSAGE_KEYS.mfaDisabled,
|
|
1149
1149
|
locale: user.locale,
|
|
1150
1150
|
data: {},
|
|
1151
1151
|
recipient: { email: user.email, phone: null, deviceToken: null }
|
|
1152
|
-
})
|
|
1153
|
-
|
|
1154
|
-
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "MFA_DISABLED", "MFA disabled successfully.");
|
|
1152
|
+
}));
|
|
1153
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "MFA_DISABLED", "MFA disabled successfully.");
|
|
1155
1154
|
}
|
|
1156
1155
|
};
|
|
1157
1156
|
}
|
|
@@ -1159,7 +1158,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1159
1158
|
// src/controllers/auth.controller.ts
|
|
1160
1159
|
var import_node_crypto5 = require("crypto");
|
|
1161
1160
|
var import_events2 = require("@fonderie/events");
|
|
1162
|
-
var
|
|
1161
|
+
var import_core6 = require("@fonderie/core");
|
|
1163
1162
|
|
|
1164
1163
|
// src/services/cooldown.ts
|
|
1165
1164
|
function checkCooldown(lastSentAt, cooldownMs) {
|
|
@@ -1374,18 +1373,18 @@ function authController(store, config, bus) {
|
|
|
1374
1373
|
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
1375
1374
|
const normalizedEmail = normalizeEmailSafe(email2);
|
|
1376
1375
|
if (!normalizedEmail) {
|
|
1377
|
-
return (0,
|
|
1376
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1378
1377
|
}
|
|
1379
1378
|
if (password2.length < 8) {
|
|
1380
|
-
return (0,
|
|
1381
|
-
|
|
1379
|
+
return (0, import_core6.setApiResponse)(
|
|
1380
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1382
1381
|
"INVALID_PARAMETER",
|
|
1383
1382
|
"password must be at least 8 characters"
|
|
1384
1383
|
);
|
|
1385
1384
|
}
|
|
1386
1385
|
const existing = await users.findByEmail(normalizedEmail);
|
|
1387
1386
|
if (existing) {
|
|
1388
|
-
return (0,
|
|
1387
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
1389
1388
|
}
|
|
1390
1389
|
const passwordHash = await hashPassword(password2);
|
|
1391
1390
|
const row = await users.create(
|
|
@@ -1395,18 +1394,18 @@ function authController(store, config, bus) {
|
|
|
1395
1394
|
lastName
|
|
1396
1395
|
);
|
|
1397
1396
|
if (!row) {
|
|
1398
|
-
return (0,
|
|
1397
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1399
1398
|
}
|
|
1400
1399
|
const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1401
1400
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1402
1401
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1403
1402
|
const user = await users.findById(row.id);
|
|
1404
1403
|
if (!user) {
|
|
1405
|
-
return (0,
|
|
1404
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1406
1405
|
}
|
|
1407
1406
|
const reqId = ctx.meta["requestId"];
|
|
1408
1407
|
const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
|
|
1409
|
-
bus?.emit(
|
|
1408
|
+
await (0, import_core6.background)(bus?.emit(
|
|
1410
1409
|
import_events2.NOTIFICATION_EVENT,
|
|
1411
1410
|
{
|
|
1412
1411
|
type: MESSAGE_KEYS.emailRegistration,
|
|
@@ -1415,9 +1414,8 @@ function authController(store, config, bus) {
|
|
|
1415
1414
|
recipient: { email: normalizedEmail, phone: null, deviceToken: null }
|
|
1416
1415
|
},
|
|
1417
1416
|
reqOpts
|
|
1418
|
-
)
|
|
1419
|
-
|
|
1420
|
-
bus?.emit(
|
|
1417
|
+
));
|
|
1418
|
+
await (0, import_core6.background)(bus?.emit(
|
|
1421
1419
|
EVENT_KEYS.userRegistered,
|
|
1422
1420
|
{
|
|
1423
1421
|
userId: user.id,
|
|
@@ -1427,8 +1425,7 @@ function authController(store, config, bus) {
|
|
|
1427
1425
|
loginMethod: "email"
|
|
1428
1426
|
},
|
|
1429
1427
|
reqOpts
|
|
1430
|
-
)
|
|
1431
|
-
});
|
|
1428
|
+
));
|
|
1432
1429
|
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1433
1430
|
loginMethod: "email"
|
|
1434
1431
|
});
|
|
@@ -1454,7 +1451,7 @@ function authController(store, config, bus) {
|
|
|
1454
1451
|
if (isValidPhone(phone2)) {
|
|
1455
1452
|
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
1456
1453
|
if (existing) {
|
|
1457
|
-
return (0,
|
|
1454
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
1458
1455
|
}
|
|
1459
1456
|
const { id } = await users.findOrCreateByPhone(
|
|
1460
1457
|
normalizePhone(phone2),
|
|
@@ -1466,11 +1463,11 @@ function authController(store, config, bus) {
|
|
|
1466
1463
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1467
1464
|
const user = await users.findById(id);
|
|
1468
1465
|
if (!user) {
|
|
1469
|
-
return (0,
|
|
1466
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1470
1467
|
}
|
|
1471
1468
|
const reqId2 = ctx.meta["requestId"];
|
|
1472
1469
|
const reqOpts2 = reqId2 !== void 0 ? { requestId: reqId2 } : void 0;
|
|
1473
|
-
bus?.emit(
|
|
1470
|
+
await (0, import_core6.background)(bus?.emit(
|
|
1474
1471
|
import_events2.NOTIFICATION_EVENT,
|
|
1475
1472
|
{
|
|
1476
1473
|
type: MESSAGE_KEYS.phoneOtp,
|
|
@@ -1479,9 +1476,8 @@ function authController(store, config, bus) {
|
|
|
1479
1476
|
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1480
1477
|
},
|
|
1481
1478
|
reqOpts2
|
|
1482
|
-
)
|
|
1483
|
-
|
|
1484
|
-
bus?.emit(
|
|
1479
|
+
));
|
|
1480
|
+
await (0, import_core6.background)(bus?.emit(
|
|
1485
1481
|
EVENT_KEYS.userRegistered,
|
|
1486
1482
|
{
|
|
1487
1483
|
userId: user.id,
|
|
@@ -1491,8 +1487,7 @@ function authController(store, config, bus) {
|
|
|
1491
1487
|
loginMethod: "phone"
|
|
1492
1488
|
},
|
|
1493
1489
|
reqOpts2
|
|
1494
|
-
)
|
|
1495
|
-
});
|
|
1490
|
+
));
|
|
1496
1491
|
const otpToken = issueMfaPendingToken(user.id, config, "phone");
|
|
1497
1492
|
return Response.json(
|
|
1498
1493
|
{
|
|
@@ -1506,8 +1501,8 @@ function authController(store, config, bus) {
|
|
|
1506
1501
|
{ status: 202 }
|
|
1507
1502
|
);
|
|
1508
1503
|
}
|
|
1509
|
-
return (0,
|
|
1510
|
-
|
|
1504
|
+
return (0, import_core6.setApiResponse)(
|
|
1505
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1511
1506
|
"INVALID_PARAMETER",
|
|
1512
1507
|
"Provide email + password or a valid phone number"
|
|
1513
1508
|
);
|
|
@@ -1519,11 +1514,11 @@ function authController(store, config, bus) {
|
|
|
1519
1514
|
const { email: rawEmail, password: password2 } = body;
|
|
1520
1515
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
1521
1516
|
if (!email2) {
|
|
1522
|
-
return (0,
|
|
1517
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1523
1518
|
}
|
|
1524
1519
|
const user = await users.findByEmail(email2);
|
|
1525
1520
|
if (!user || !user.passwordHash) {
|
|
1526
|
-
loginEvents.recordSafe({
|
|
1521
|
+
await loginEvents.recordSafe({
|
|
1527
1522
|
userId: null,
|
|
1528
1523
|
emailAttempted: email2,
|
|
1529
1524
|
method: "password",
|
|
@@ -1531,7 +1526,7 @@ function authController(store, config, bus) {
|
|
|
1531
1526
|
failureReason: "unknown_email",
|
|
1532
1527
|
...meta
|
|
1533
1528
|
});
|
|
1534
|
-
return (0,
|
|
1529
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1535
1530
|
}
|
|
1536
1531
|
const { valid, needsRehash } = await verifyPasswordForLogin(
|
|
1537
1532
|
password2,
|
|
@@ -1539,7 +1534,7 @@ function authController(store, config, bus) {
|
|
|
1539
1534
|
config.legacyVerify
|
|
1540
1535
|
);
|
|
1541
1536
|
if (!valid) {
|
|
1542
|
-
loginEvents.recordSafe({
|
|
1537
|
+
await loginEvents.recordSafe({
|
|
1543
1538
|
userId: user.id,
|
|
1544
1539
|
emailAttempted: email2,
|
|
1545
1540
|
method: "password",
|
|
@@ -1547,21 +1542,21 @@ function authController(store, config, bus) {
|
|
|
1547
1542
|
failureReason: "bad_password",
|
|
1548
1543
|
...meta
|
|
1549
1544
|
});
|
|
1550
|
-
return (0,
|
|
1545
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1551
1546
|
}
|
|
1552
1547
|
if (needsRehash) {
|
|
1553
1548
|
await users.updatePassword(user.id, await hashPassword(password2));
|
|
1554
1549
|
}
|
|
1555
1550
|
if (user.suspended) {
|
|
1556
|
-
return (0,
|
|
1557
|
-
|
|
1551
|
+
return (0, import_core6.setApiResponse)(
|
|
1552
|
+
import_core6.HTTP.FORBIDDEN,
|
|
1558
1553
|
"ACCOUNT_SUSPENDED",
|
|
1559
1554
|
"Account suspended. Please contact support."
|
|
1560
1555
|
);
|
|
1561
1556
|
}
|
|
1562
1557
|
if (user.mfaEnabled) {
|
|
1563
1558
|
const mfaToken = issueMfaPendingToken(user.id, config, "email");
|
|
1564
|
-
return (0,
|
|
1559
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
|
|
1565
1560
|
mfaToken
|
|
1566
1561
|
});
|
|
1567
1562
|
}
|
|
@@ -1569,7 +1564,7 @@ function authController(store, config, bus) {
|
|
|
1569
1564
|
loginMethod: "email"
|
|
1570
1565
|
});
|
|
1571
1566
|
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
1572
|
-
loginEvents.recordSafe({
|
|
1567
|
+
await loginEvents.recordSafe({
|
|
1573
1568
|
userId: user.id,
|
|
1574
1569
|
emailAttempted: email2,
|
|
1575
1570
|
method: "password",
|
|
@@ -1598,11 +1593,11 @@ function authController(store, config, bus) {
|
|
|
1598
1593
|
if (isValidPhone(phone2)) {
|
|
1599
1594
|
const user = await users.findByPhone(normalizePhone(phone2));
|
|
1600
1595
|
if (!user) {
|
|
1601
|
-
return (0,
|
|
1596
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1602
1597
|
}
|
|
1603
1598
|
if (user.suspended) {
|
|
1604
|
-
return (0,
|
|
1605
|
-
|
|
1599
|
+
return (0, import_core6.setApiResponse)(
|
|
1600
|
+
import_core6.HTTP.FORBIDDEN,
|
|
1606
1601
|
"ACCOUNT_SUSPENDED",
|
|
1607
1602
|
"Account suspended. Please contact support."
|
|
1608
1603
|
);
|
|
@@ -1610,13 +1605,12 @@ function authController(store, config, bus) {
|
|
|
1610
1605
|
const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1611
1606
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1612
1607
|
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1613
|
-
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1608
|
+
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1614
1609
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1615
1610
|
locale: user.locale,
|
|
1616
1611
|
data: { otp },
|
|
1617
1612
|
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1618
|
-
})
|
|
1619
|
-
});
|
|
1613
|
+
}));
|
|
1620
1614
|
const otpToken = issueMfaPendingToken(user.id, config, "phone");
|
|
1621
1615
|
return Response.json(
|
|
1622
1616
|
{
|
|
@@ -1627,8 +1621,8 @@ function authController(store, config, bus) {
|
|
|
1627
1621
|
{ status: 202 }
|
|
1628
1622
|
);
|
|
1629
1623
|
}
|
|
1630
|
-
return (0,
|
|
1631
|
-
|
|
1624
|
+
return (0, import_core6.setApiResponse)(
|
|
1625
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1632
1626
|
"INVALID_PARAMETER",
|
|
1633
1627
|
"Provide email + password or a valid phone number"
|
|
1634
1628
|
);
|
|
@@ -1653,23 +1647,23 @@ function authController(store, config, bus) {
|
|
|
1653
1647
|
refresh: async (ctx) => {
|
|
1654
1648
|
const token = extractRefreshToken(ctx);
|
|
1655
1649
|
if (!token) {
|
|
1656
|
-
return (0,
|
|
1650
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
|
|
1657
1651
|
}
|
|
1658
1652
|
const payload = verifyToken(token, config);
|
|
1659
1653
|
if (!payload || payload.type !== "refresh") {
|
|
1660
|
-
return (0,
|
|
1654
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
1661
1655
|
}
|
|
1662
1656
|
const valid = await sessions.exists(token);
|
|
1663
1657
|
if (!valid) {
|
|
1664
|
-
return (0,
|
|
1665
|
-
|
|
1658
|
+
return (0, import_core6.setApiResponse)(
|
|
1659
|
+
import_core6.HTTP.UNAUTHORIZED,
|
|
1666
1660
|
"TOKEN_REFRESH_FAILED",
|
|
1667
1661
|
"Session expired or already revoked"
|
|
1668
1662
|
);
|
|
1669
1663
|
}
|
|
1670
1664
|
const user = await users.findById(payload.sub);
|
|
1671
1665
|
if (!user || user.suspended || user.deletedAt) {
|
|
1672
|
-
return (0,
|
|
1666
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1673
1667
|
}
|
|
1674
1668
|
await sessions.delete(token);
|
|
1675
1669
|
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
@@ -1693,16 +1687,16 @@ function authController(store, config, bus) {
|
|
|
1693
1687
|
const body = ctx.meta["body"];
|
|
1694
1688
|
const rawEmail = body?.["email"];
|
|
1695
1689
|
if (typeof rawEmail !== "string") {
|
|
1696
|
-
return (0,
|
|
1690
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
1697
1691
|
}
|
|
1698
1692
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
1699
1693
|
if (!email2) {
|
|
1700
|
-
return (0,
|
|
1694
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1701
1695
|
}
|
|
1702
1696
|
const user = await users.findByEmail(email2);
|
|
1703
1697
|
if (!user) {
|
|
1704
|
-
return (0,
|
|
1705
|
-
|
|
1698
|
+
return (0, import_core6.setApiResponse)(
|
|
1699
|
+
import_core6.HTTP.OK,
|
|
1706
1700
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
1707
1701
|
"Password reset email sent (if account exists)."
|
|
1708
1702
|
);
|
|
@@ -1711,8 +1705,8 @@ function authController(store, config, bus) {
|
|
|
1711
1705
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1712
1706
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
1713
1707
|
if (remaining > 0) {
|
|
1714
|
-
return (0,
|
|
1715
|
-
|
|
1708
|
+
return (0, import_core6.setApiResponse)(
|
|
1709
|
+
import_core6.HTTP.OK,
|
|
1716
1710
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
1717
1711
|
"Password reset email sent (if account exists)."
|
|
1718
1712
|
);
|
|
@@ -1723,15 +1717,14 @@ function authController(store, config, bus) {
|
|
|
1723
1717
|
await passwordReset.create(user.id, pin, token, expiresAt);
|
|
1724
1718
|
const base = resolved.passwordResetUrl;
|
|
1725
1719
|
const resetUrl = base ? `${base}${base.includes("?") ? "&" : "?"}token=${token}` : "";
|
|
1726
|
-
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1720
|
+
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1727
1721
|
type: MESSAGE_KEYS.passwordReset,
|
|
1728
1722
|
locale: user.locale,
|
|
1729
1723
|
recipient: { email: email2, phone: null, deviceToken: null },
|
|
1730
1724
|
data: { pin, token, resetUrl }
|
|
1731
|
-
})
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
import_core5.HTTP.OK,
|
|
1725
|
+
}));
|
|
1726
|
+
return (0, import_core6.setApiResponse)(
|
|
1727
|
+
import_core6.HTTP.OK,
|
|
1735
1728
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
1736
1729
|
"Password reset email sent (if account exists)."
|
|
1737
1730
|
);
|
|
@@ -1742,11 +1735,11 @@ function authController(store, config, bus) {
|
|
|
1742
1735
|
const password2 = body?.["password"];
|
|
1743
1736
|
const rawToken = body?.["token"];
|
|
1744
1737
|
if (typeof password2 !== "string") {
|
|
1745
|
-
return (0,
|
|
1738
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
|
|
1746
1739
|
}
|
|
1747
1740
|
if (password2.length < 8) {
|
|
1748
|
-
return (0,
|
|
1749
|
-
|
|
1741
|
+
return (0, import_core6.setApiResponse)(
|
|
1742
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1750
1743
|
"INVALID_PARAMETER",
|
|
1751
1744
|
"password must be at least 8 characters"
|
|
1752
1745
|
);
|
|
@@ -1756,8 +1749,8 @@ function authController(store, config, bus) {
|
|
|
1756
1749
|
row = await passwordReset.findByToken(rawToken.trim());
|
|
1757
1750
|
} else {
|
|
1758
1751
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
1759
|
-
return (0,
|
|
1760
|
-
|
|
1752
|
+
return (0, import_core6.setApiResponse)(
|
|
1753
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1761
1754
|
"INVALID_PARAMETER",
|
|
1762
1755
|
"a 6-digit pin or a reset token is required"
|
|
1763
1756
|
);
|
|
@@ -1765,8 +1758,8 @@ function authController(store, config, bus) {
|
|
|
1765
1758
|
row = await passwordReset.findByPin(raw.trim());
|
|
1766
1759
|
}
|
|
1767
1760
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1768
|
-
return (0,
|
|
1769
|
-
|
|
1761
|
+
return (0, import_core6.setApiResponse)(
|
|
1762
|
+
import_core6.HTTP.BAD_REQUEST,
|
|
1770
1763
|
"PASSWORD_RESET_FAILED",
|
|
1771
1764
|
"Invalid or expired reset credentials"
|
|
1772
1765
|
);
|
|
@@ -1784,14 +1777,14 @@ function authController(store, config, bus) {
|
|
|
1784
1777
|
tx.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [row.userId])
|
|
1785
1778
|
]);
|
|
1786
1779
|
});
|
|
1787
|
-
return (0,
|
|
1780
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
|
|
1788
1781
|
},
|
|
1789
1782
|
verify: async (ctx) => {
|
|
1790
1783
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
1791
|
-
return (0,
|
|
1784
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
1792
1785
|
}
|
|
1793
1786
|
if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
|
|
1794
|
-
return (0,
|
|
1787
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
1795
1788
|
verified: true,
|
|
1796
1789
|
email: ctx.user.email
|
|
1797
1790
|
});
|
|
@@ -1799,8 +1792,8 @@ function authController(store, config, bus) {
|
|
|
1799
1792
|
const body = ctx.meta["body"];
|
|
1800
1793
|
const raw = body?.["token"];
|
|
1801
1794
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
1802
|
-
return (0,
|
|
1803
|
-
|
|
1795
|
+
return (0, import_core6.setApiResponse)(
|
|
1796
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1804
1797
|
"INVALID_PARAMETER",
|
|
1805
1798
|
"token must be a 6-digit code"
|
|
1806
1799
|
);
|
|
@@ -1809,12 +1802,12 @@ function authController(store, config, bus) {
|
|
|
1809
1802
|
if (ctx.user.loginMethod === "phone") {
|
|
1810
1803
|
const record = await phoneVerif.findByUser(ctx.user.id, pin);
|
|
1811
1804
|
if (!record) {
|
|
1812
|
-
return (0,
|
|
1805
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
1813
1806
|
}
|
|
1814
1807
|
if (/* @__PURE__ */ new Date() > record.expiresAt) {
|
|
1815
1808
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
1816
|
-
return (0,
|
|
1817
|
-
|
|
1809
|
+
return (0, import_core6.setApiResponse)(
|
|
1810
|
+
import_core6.HTTP.BAD_REQUEST,
|
|
1818
1811
|
"VERIFICATION_FAILED",
|
|
1819
1812
|
"Verification code expired"
|
|
1820
1813
|
);
|
|
@@ -1827,11 +1820,11 @@ function authController(store, config, bus) {
|
|
|
1827
1820
|
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));
|
|
1828
1821
|
const verifiedUser = await users.findById(ctx.user.id);
|
|
1829
1822
|
if (!verifiedUser) {
|
|
1830
|
-
return (0,
|
|
1823
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
1831
1824
|
}
|
|
1832
1825
|
if (verifiedUser.suspended) {
|
|
1833
|
-
return (0,
|
|
1834
|
-
|
|
1826
|
+
return (0, import_core6.setApiResponse)(
|
|
1827
|
+
import_core6.HTTP.FORBIDDEN,
|
|
1835
1828
|
"ACCOUNT_SUSPENDED",
|
|
1836
1829
|
"Account suspended. Please contact support."
|
|
1837
1830
|
);
|
|
@@ -1853,10 +1846,10 @@ function authController(store, config, bus) {
|
|
|
1853
1846
|
}
|
|
1854
1847
|
const row = await emailVerif.findByUser(ctx.user.id, pin);
|
|
1855
1848
|
if (!row) {
|
|
1856
|
-
return (0,
|
|
1849
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
1857
1850
|
}
|
|
1858
1851
|
if (/* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1859
|
-
return (0,
|
|
1852
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
|
|
1860
1853
|
}
|
|
1861
1854
|
await store.transaction(async (tx) => {
|
|
1862
1855
|
await Promise.all([
|
|
@@ -1870,30 +1863,30 @@ function authController(store, config, bus) {
|
|
|
1870
1863
|
])
|
|
1871
1864
|
]);
|
|
1872
1865
|
});
|
|
1873
|
-
return (0,
|
|
1866
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
1874
1867
|
verified: true,
|
|
1875
1868
|
email: ctx.user.email
|
|
1876
1869
|
});
|
|
1877
1870
|
},
|
|
1878
1871
|
sendVerification: async (ctx) => {
|
|
1879
1872
|
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
1880
|
-
return (0,
|
|
1873
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
1881
1874
|
}
|
|
1882
1875
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
1883
1876
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1884
1877
|
if (ctx.user.loginMethod === "phone") {
|
|
1885
1878
|
const phone2 = ctx.user.phone;
|
|
1886
1879
|
if (!phone2) {
|
|
1887
|
-
return (0,
|
|
1888
|
-
|
|
1880
|
+
return (0, import_core6.setApiResponse)(
|
|
1881
|
+
import_core6.HTTP.BAD_REQUEST,
|
|
1889
1882
|
"NO_PHONE_ON_ACCOUNT",
|
|
1890
1883
|
"No phone number associated with this account"
|
|
1891
1884
|
);
|
|
1892
1885
|
}
|
|
1893
1886
|
const remaining2 = checkCooldown(await phoneVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
1894
1887
|
if (remaining2 > 0) {
|
|
1895
|
-
return (0,
|
|
1896
|
-
|
|
1888
|
+
return (0, import_core6.setApiResponse)(
|
|
1889
|
+
import_core6.HTTP.TOO_MANY_REQUESTS,
|
|
1897
1890
|
"VERIFICATION_COOLDOWN",
|
|
1898
1891
|
"Please wait before requesting a new code.",
|
|
1899
1892
|
{
|
|
@@ -1904,36 +1897,35 @@ function authController(store, config, bus) {
|
|
|
1904
1897
|
const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1905
1898
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
1906
1899
|
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
1907
|
-
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1900
|
+
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1908
1901
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1909
1902
|
locale: ctx.user.locale,
|
|
1910
1903
|
data: { otp },
|
|
1911
1904
|
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
1912
|
-
})
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
import_core5.HTTP.OK,
|
|
1905
|
+
}));
|
|
1906
|
+
return (0, import_core6.setApiResponse)(
|
|
1907
|
+
import_core6.HTTP.OK,
|
|
1916
1908
|
"VERIFICATION_SENT",
|
|
1917
1909
|
"A verification code has been sent to your phone."
|
|
1918
1910
|
);
|
|
1919
1911
|
}
|
|
1920
1912
|
if (!ctx.user.email) {
|
|
1921
|
-
return (0,
|
|
1922
|
-
|
|
1913
|
+
return (0, import_core6.setApiResponse)(
|
|
1914
|
+
import_core6.HTTP.BAD_REQUEST,
|
|
1923
1915
|
"NO_EMAIL_ON_ACCOUNT",
|
|
1924
1916
|
"No email address associated with this account"
|
|
1925
1917
|
);
|
|
1926
1918
|
}
|
|
1927
1919
|
if (ctx.user.emailVerifiedAt) {
|
|
1928
|
-
return (0,
|
|
1920
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "EMAIL_VERIFIED", "Email already verified.", {
|
|
1929
1921
|
verified: true,
|
|
1930
1922
|
email: ctx.user.email
|
|
1931
1923
|
});
|
|
1932
1924
|
}
|
|
1933
1925
|
const remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
1934
1926
|
if (remaining > 0) {
|
|
1935
|
-
return (0,
|
|
1936
|
-
|
|
1927
|
+
return (0, import_core6.setApiResponse)(
|
|
1928
|
+
import_core6.HTTP.TOO_MANY_REQUESTS,
|
|
1937
1929
|
"VERIFICATION_COOLDOWN",
|
|
1938
1930
|
"Please wait before requesting a new code.",
|
|
1939
1931
|
{
|
|
@@ -1944,14 +1936,13 @@ function authController(store, config, bus) {
|
|
|
1944
1936
|
const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1945
1937
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1946
1938
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
1947
|
-
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1939
|
+
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1948
1940
|
type: MESSAGE_KEYS.emailVerification,
|
|
1949
1941
|
locale: ctx.user.locale,
|
|
1950
1942
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null },
|
|
1951
1943
|
data: { pin }
|
|
1952
|
-
})
|
|
1953
|
-
|
|
1954
|
-
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
|
|
1944
|
+
}));
|
|
1945
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
|
|
1955
1946
|
email: ctx.user.email
|
|
1956
1947
|
});
|
|
1957
1948
|
}
|
|
@@ -1960,11 +1951,11 @@ function authController(store, config, bus) {
|
|
|
1960
1951
|
|
|
1961
1952
|
// src/controllers/user.controller.ts
|
|
1962
1953
|
var import_node_crypto6 = require("crypto");
|
|
1963
|
-
var
|
|
1954
|
+
var import_core8 = require("@fonderie/core");
|
|
1964
1955
|
var import_events3 = require("@fonderie/events");
|
|
1965
1956
|
|
|
1966
1957
|
// src/dtos/login-activity.ts
|
|
1967
|
-
var
|
|
1958
|
+
var import_core7 = require("@fonderie/core");
|
|
1968
1959
|
function toLoginEventDTO(row) {
|
|
1969
1960
|
return {
|
|
1970
1961
|
id: row.id,
|
|
@@ -1978,10 +1969,10 @@ function toLoginEventDTO(row) {
|
|
|
1978
1969
|
}
|
|
1979
1970
|
function toLoginHistoryPageDTO(page) {
|
|
1980
1971
|
const last = page.events[page.events.length - 1];
|
|
1981
|
-
const nextCursor = page.hasMore && last ? (0,
|
|
1972
|
+
const nextCursor = page.hasMore && last ? (0, import_core7.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
|
|
1982
1973
|
return { events: page.events.map(toLoginEventDTO), nextCursor };
|
|
1983
1974
|
}
|
|
1984
|
-
var decodeLoginCursor =
|
|
1975
|
+
var decodeLoginCursor = import_core7.decodeKeysetCursor;
|
|
1985
1976
|
function toSessionDTO(row, currentSid) {
|
|
1986
1977
|
return {
|
|
1987
1978
|
id: row.id,
|
|
@@ -2010,9 +2001,9 @@ function userController(store, config, bus) {
|
|
|
2010
2001
|
me: async (ctx) => {
|
|
2011
2002
|
const user = await users.findById(ctx.user.id);
|
|
2012
2003
|
if (!user) {
|
|
2013
|
-
return (0,
|
|
2004
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2014
2005
|
}
|
|
2015
|
-
return (0,
|
|
2006
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
|
|
2016
2007
|
user: toUserDTO(user, ctx.user.phoneVerified)
|
|
2017
2008
|
});
|
|
2018
2009
|
},
|
|
@@ -2023,14 +2014,14 @@ function userController(store, config, bus) {
|
|
|
2023
2014
|
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
2024
2015
|
const outcomeParam = params.get("outcome");
|
|
2025
2016
|
if (outcomeParam !== null && outcomeParam !== "success" && outcomeParam !== "failed") {
|
|
2026
|
-
return (0,
|
|
2017
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
|
|
2027
2018
|
}
|
|
2028
2019
|
const cursorParam = params.get("cursor");
|
|
2029
2020
|
let cursor = null;
|
|
2030
2021
|
if (cursorParam) {
|
|
2031
2022
|
cursor = decodeLoginCursor(cursorParam);
|
|
2032
2023
|
if (!cursor) {
|
|
2033
|
-
return (0,
|
|
2024
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
2034
2025
|
}
|
|
2035
2026
|
}
|
|
2036
2027
|
const query = { userId: ctx.user.id, limit };
|
|
@@ -2041,8 +2032,8 @@ function userController(store, config, bus) {
|
|
|
2041
2032
|
if (from) query.from = new Date(from);
|
|
2042
2033
|
if (to) query.to = new Date(to);
|
|
2043
2034
|
const page = await loginEvents.listByUser(query);
|
|
2044
|
-
return (0,
|
|
2045
|
-
|
|
2035
|
+
return (0, import_core8.setApiResponse)(
|
|
2036
|
+
import_core8.HTTP.OK,
|
|
2046
2037
|
"LOGIN_HISTORY_FETCHED",
|
|
2047
2038
|
"Login history retrieved.",
|
|
2048
2039
|
toLoginHistoryPageDTO(page)
|
|
@@ -2052,32 +2043,32 @@ function userController(store, config, bus) {
|
|
|
2052
2043
|
listSessions: async (ctx) => {
|
|
2053
2044
|
const rows = await sessions.listLiveByUser(ctx.user.id);
|
|
2054
2045
|
const currentSid = ctx.user.sid ?? null;
|
|
2055
|
-
return (0,
|
|
2046
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
|
|
2056
2047
|
sessions: rows.map((r) => toSessionDTO(r, currentSid))
|
|
2057
2048
|
});
|
|
2058
2049
|
},
|
|
2059
2050
|
terminateSession: async (ctx) => {
|
|
2060
2051
|
const id = ctx.meta["params"]?.["id"];
|
|
2061
2052
|
if (!id) {
|
|
2062
|
-
return (0,
|
|
2053
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2063
2054
|
}
|
|
2064
2055
|
const removed = await sessions.terminateById(ctx.user.id, id);
|
|
2065
2056
|
if (!removed) {
|
|
2066
|
-
return (0,
|
|
2057
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2067
2058
|
}
|
|
2068
|
-
return (0,
|
|
2059
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSION_TERMINATED", "Session terminated.", { id });
|
|
2069
2060
|
},
|
|
2070
2061
|
terminateOtherSessions: async (ctx) => {
|
|
2071
2062
|
const currentSid = ctx.user.sid ?? null;
|
|
2072
2063
|
if (!currentSid) {
|
|
2073
|
-
return (0,
|
|
2074
|
-
|
|
2064
|
+
return (0, import_core8.setApiResponse)(
|
|
2065
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2075
2066
|
"NO_CURRENT_SESSION",
|
|
2076
2067
|
"Cannot terminate others without a current session context"
|
|
2077
2068
|
);
|
|
2078
2069
|
}
|
|
2079
2070
|
const count = await sessions.terminateOthers(ctx.user.id, currentSid);
|
|
2080
|
-
return (0,
|
|
2071
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
|
|
2081
2072
|
},
|
|
2082
2073
|
updateProfile: async (ctx) => {
|
|
2083
2074
|
const body = ctx.meta["body"];
|
|
@@ -2087,18 +2078,18 @@ function userController(store, config, bus) {
|
|
|
2087
2078
|
if (body?.[key] !== void 0) fields[key] = body[key];
|
|
2088
2079
|
}
|
|
2089
2080
|
if (Object.keys(fields).length === 0) {
|
|
2090
|
-
return (0,
|
|
2091
|
-
|
|
2081
|
+
return (0, import_core8.setApiResponse)(
|
|
2082
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2092
2083
|
"INVALID_PARAMETER",
|
|
2093
2084
|
"Provide at least one of: firstName, lastName, avatarUrl"
|
|
2094
2085
|
);
|
|
2095
2086
|
}
|
|
2096
2087
|
const row = await users.update(ctx.user.id, fields);
|
|
2097
2088
|
if (!row) {
|
|
2098
|
-
return (0,
|
|
2089
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2099
2090
|
}
|
|
2100
2091
|
const updated = await users.findById(ctx.user.id);
|
|
2101
|
-
return (0,
|
|
2092
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PROFILE_UPDATED", "Profile updated.", {
|
|
2102
2093
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2103
2094
|
});
|
|
2104
2095
|
},
|
|
@@ -2115,14 +2106,14 @@ function userController(store, config, bus) {
|
|
|
2115
2106
|
if (Object.keys(patch).length > 0) fields.patch = patch;
|
|
2116
2107
|
const row = await users.updatePreferences(ctx.user.id, fields);
|
|
2117
2108
|
if (!row) {
|
|
2118
|
-
return (0,
|
|
2119
|
-
|
|
2109
|
+
return (0, import_core8.setApiResponse)(
|
|
2110
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2120
2111
|
"INVALID_PARAMETER",
|
|
2121
2112
|
"Provide at least one preference field"
|
|
2122
2113
|
);
|
|
2123
2114
|
}
|
|
2124
2115
|
const updated = await users.findById(ctx.user.id);
|
|
2125
|
-
return (0,
|
|
2116
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
|
|
2126
2117
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
2127
2118
|
});
|
|
2128
2119
|
},
|
|
@@ -2131,46 +2122,44 @@ function userController(store, config, bus) {
|
|
|
2131
2122
|
const newEmail = body?.["email"];
|
|
2132
2123
|
const normalised = typeof newEmail === "string" ? normalizeEmailSafe(newEmail) : null;
|
|
2133
2124
|
if (!normalised) {
|
|
2134
|
-
return (0,
|
|
2135
|
-
|
|
2125
|
+
return (0, import_core8.setApiResponse)(
|
|
2126
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2136
2127
|
"INVALID_PARAMETER",
|
|
2137
2128
|
"A valid email address is required"
|
|
2138
2129
|
);
|
|
2139
2130
|
}
|
|
2140
2131
|
const oldEmail = ctx.user.email;
|
|
2141
2132
|
if (normalised === oldEmail) {
|
|
2142
|
-
return (0,
|
|
2143
|
-
|
|
2133
|
+
return (0, import_core8.setApiResponse)(
|
|
2134
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2144
2135
|
"INVALID_PARAMETER",
|
|
2145
2136
|
"New email must differ from current email"
|
|
2146
2137
|
);
|
|
2147
2138
|
}
|
|
2148
2139
|
const existing = await users.findByEmail(normalised);
|
|
2149
2140
|
if (existing) {
|
|
2150
|
-
return (0,
|
|
2141
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
2151
2142
|
}
|
|
2152
2143
|
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2153
2144
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2154
2145
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2155
2146
|
await users.updateEmail(ctx.user.id, normalised);
|
|
2156
|
-
bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2147
|
+
await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2157
2148
|
type: MESSAGE_KEYS.emailVerification,
|
|
2158
2149
|
locale: ctx.user.locale,
|
|
2159
2150
|
data: { pin },
|
|
2160
2151
|
recipient: { email: normalised, phone: null, deviceToken: null }
|
|
2161
|
-
})
|
|
2162
|
-
});
|
|
2152
|
+
}));
|
|
2163
2153
|
if (oldEmail) {
|
|
2164
|
-
bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2154
|
+
await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2165
2155
|
type: MESSAGE_KEYS.emailChanged,
|
|
2166
2156
|
locale: ctx.user.locale,
|
|
2167
2157
|
data: { newEmail: normalised },
|
|
2168
2158
|
recipient: { email: oldEmail, phone: null, deviceToken: null }
|
|
2169
|
-
})
|
|
2170
|
-
});
|
|
2159
|
+
}));
|
|
2171
2160
|
}
|
|
2172
|
-
return (0,
|
|
2173
|
-
|
|
2161
|
+
return (0, import_core8.setApiResponse)(
|
|
2162
|
+
import_core8.HTTP.OK,
|
|
2174
2163
|
"EMAIL_UPDATED",
|
|
2175
2164
|
"Email updated. A verification code has been sent to your new address.",
|
|
2176
2165
|
{
|
|
@@ -2182,8 +2171,8 @@ function userController(store, config, bus) {
|
|
|
2182
2171
|
const body = ctx.meta["body"];
|
|
2183
2172
|
const newPhone = body?.["phone"];
|
|
2184
2173
|
if (!isValidPhone2(newPhone)) {
|
|
2185
|
-
return (0,
|
|
2186
|
-
|
|
2174
|
+
return (0, import_core8.setApiResponse)(
|
|
2175
|
+
import_core8.HTTP.UNPROCESSABLE,
|
|
2187
2176
|
"INVALID_PARAMETER",
|
|
2188
2177
|
"A valid phone number is required"
|
|
2189
2178
|
);
|
|
@@ -2191,30 +2180,28 @@ function userController(store, config, bus) {
|
|
|
2191
2180
|
const normalised = normalizePhone2(newPhone);
|
|
2192
2181
|
const existing = await users.findByPhone(normalised);
|
|
2193
2182
|
if (existing) {
|
|
2194
|
-
return (0,
|
|
2183
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
2195
2184
|
}
|
|
2196
2185
|
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2197
2186
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
2198
2187
|
await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
|
|
2199
2188
|
await users.updatePhone(ctx.user.id, normalised);
|
|
2200
|
-
bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2189
|
+
await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2201
2190
|
type: MESSAGE_KEYS.phoneOtp,
|
|
2202
2191
|
locale: ctx.user.locale,
|
|
2203
2192
|
data: { otp },
|
|
2204
2193
|
recipient: { email: null, phone: normalised, deviceToken: null }
|
|
2205
|
-
})
|
|
2206
|
-
});
|
|
2194
|
+
}));
|
|
2207
2195
|
if (ctx.user.email) {
|
|
2208
|
-
bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2196
|
+
await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
2209
2197
|
type: MESSAGE_KEYS.phoneChanged,
|
|
2210
2198
|
locale: ctx.user.locale,
|
|
2211
2199
|
data: {},
|
|
2212
2200
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
2213
|
-
})
|
|
2214
|
-
});
|
|
2201
|
+
}));
|
|
2215
2202
|
}
|
|
2216
|
-
return (0,
|
|
2217
|
-
|
|
2203
|
+
return (0, import_core8.setApiResponse)(
|
|
2204
|
+
import_core8.HTTP.OK,
|
|
2218
2205
|
"PHONE_UPDATED",
|
|
2219
2206
|
"Phone number updated. A verification code has been sent to your new number.",
|
|
2220
2207
|
{
|
|
@@ -2227,33 +2214,32 @@ function userController(store, config, bus) {
|
|
|
2227
2214
|
const currentPassword = body?.["currentPassword"];
|
|
2228
2215
|
const newPassword = body?.["newPassword"];
|
|
2229
2216
|
if (typeof currentPassword !== "string" || !currentPassword) {
|
|
2230
|
-
return (0,
|
|
2217
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
|
|
2231
2218
|
}
|
|
2232
2219
|
if (typeof newPassword !== "string" || newPassword.length < 8) {
|
|
2233
|
-
return (0,
|
|
2220
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
|
|
2234
2221
|
}
|
|
2235
2222
|
const user = await users.findById(ctx.user.id);
|
|
2236
|
-
if (!user) return (0,
|
|
2223
|
+
if (!user) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2237
2224
|
const { hashPassword: hashPassword2, verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password(), password_exports));
|
|
2238
2225
|
const valid = await verifyPassword2(currentPassword, user.passwordHash ?? "");
|
|
2239
2226
|
if (!valid) {
|
|
2240
|
-
return (0,
|
|
2227
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
|
|
2241
2228
|
}
|
|
2242
2229
|
const hash = await hashPassword2(newPassword);
|
|
2243
2230
|
await users.updatePassword(ctx.user.id, hash);
|
|
2244
2231
|
await sessions.deleteByUser(ctx.user.id);
|
|
2245
|
-
return (0,
|
|
2232
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PASSWORD_CHANGED", "Password updated successfully.");
|
|
2246
2233
|
},
|
|
2247
2234
|
deleteMe: async (ctx) => {
|
|
2248
2235
|
const userId = ctx.user.id;
|
|
2249
2236
|
await users.softDelete(userId);
|
|
2250
2237
|
const reqId = ctx.meta["requestId"];
|
|
2251
|
-
bus?.emit(
|
|
2238
|
+
await (0, import_core8.background)(bus?.emit(
|
|
2252
2239
|
EVENT_KEYS.userDeleted,
|
|
2253
2240
|
{ userId },
|
|
2254
2241
|
reqId !== void 0 ? { requestId: reqId } : void 0
|
|
2255
|
-
)
|
|
2256
|
-
});
|
|
2242
|
+
));
|
|
2257
2243
|
return Response.json(
|
|
2258
2244
|
{ reason: "ACCOUNT_DELETED", explanation: "Account successfully deleted." },
|
|
2259
2245
|
{
|
|
@@ -2267,7 +2253,7 @@ function userController(store, config, bus) {
|
|
|
2267
2253
|
// no MFA secret, no session tokens (session metadata only).
|
|
2268
2254
|
exportMe: async (ctx) => {
|
|
2269
2255
|
const user = await users.findById(ctx.user.id);
|
|
2270
|
-
if (!user) return (0,
|
|
2256
|
+
if (!user) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
2271
2257
|
const sessionRows = await sessions.listByUser(ctx.user.id);
|
|
2272
2258
|
const bundle = {
|
|
2273
2259
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2276,8 +2262,8 @@ function userController(store, config, bus) {
|
|
|
2276
2262
|
id: s.id,
|
|
2277
2263
|
userAgent: s.userAgent,
|
|
2278
2264
|
ipAddress: s.ipAddress,
|
|
2279
|
-
createdAt: (0,
|
|
2280
|
-
expiresAt: (0,
|
|
2265
|
+
createdAt: (0, import_core8.dateOrEmpty)(s.createdAt),
|
|
2266
|
+
expiresAt: (0, import_core8.dateOrEmpty)(s.expiresAt)
|
|
2281
2267
|
})),
|
|
2282
2268
|
security: {
|
|
2283
2269
|
mfaEnabled: user.mfaEnabled === true,
|
|
@@ -2296,7 +2282,7 @@ function userController(store, config, bus) {
|
|
|
2296
2282
|
}
|
|
2297
2283
|
bundle["modules"] = modules;
|
|
2298
2284
|
}
|
|
2299
|
-
return (0,
|
|
2285
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "DATA_EXPORT", "Your account data.", bundle);
|
|
2300
2286
|
}
|
|
2301
2287
|
};
|
|
2302
2288
|
}
|
|
@@ -2304,7 +2290,7 @@ function userController(store, config, bus) {
|
|
|
2304
2290
|
// src/controllers/oauth.controller.ts
|
|
2305
2291
|
var import_node_crypto7 = require("crypto");
|
|
2306
2292
|
var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
|
|
2307
|
-
var
|
|
2293
|
+
var import_core9 = require("@fonderie/core");
|
|
2308
2294
|
|
|
2309
2295
|
// src/models/consumed-token.model.ts
|
|
2310
2296
|
var ConsumedTokenModel = class {
|
|
@@ -2413,18 +2399,18 @@ function oauthController(store, config) {
|
|
|
2413
2399
|
const completeAppleLogin = async (ctx, claims) => {
|
|
2414
2400
|
const meta = requestMeta(ctx);
|
|
2415
2401
|
if (!claims.email) {
|
|
2416
|
-
return (0,
|
|
2402
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "No email in Apple identity token");
|
|
2417
2403
|
}
|
|
2418
2404
|
if (!claims.emailVerified) {
|
|
2419
|
-
return (0,
|
|
2405
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Apple account email is not verified");
|
|
2420
2406
|
}
|
|
2421
2407
|
const normalizedEmail = normalizeEmailSafe(claims.email);
|
|
2422
2408
|
if (!normalizedEmail) {
|
|
2423
|
-
return (0,
|
|
2409
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid email in Apple identity token");
|
|
2424
2410
|
}
|
|
2425
2411
|
const upserted = await users.upsertByProvider(normalizedEmail, "apple", claims.sub ?? "");
|
|
2426
2412
|
if (!upserted) {
|
|
2427
|
-
loginEvents.recordSafe({
|
|
2413
|
+
await loginEvents.recordSafe({
|
|
2428
2414
|
userId: null,
|
|
2429
2415
|
emailAttempted: normalizedEmail,
|
|
2430
2416
|
method: "oauth-apple",
|
|
@@ -2432,15 +2418,15 @@ function oauthController(store, config) {
|
|
|
2432
2418
|
failureReason: "provider_upsert_failed",
|
|
2433
2419
|
...meta
|
|
2434
2420
|
});
|
|
2435
|
-
return (0,
|
|
2421
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
|
|
2436
2422
|
}
|
|
2437
2423
|
const fullUser = await users.findById(upserted.id);
|
|
2438
2424
|
if (!fullUser) {
|
|
2439
|
-
return (0,
|
|
2425
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
|
|
2440
2426
|
}
|
|
2441
2427
|
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, { loginMethod: "apple" });
|
|
2442
2428
|
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
2443
|
-
loginEvents.recordSafe({
|
|
2429
|
+
await loginEvents.recordSafe({
|
|
2444
2430
|
userId: upserted.id,
|
|
2445
2431
|
emailAttempted: normalizedEmail,
|
|
2446
2432
|
method: "oauth-apple",
|
|
@@ -2470,8 +2456,8 @@ function oauthController(store, config) {
|
|
|
2470
2456
|
googleInit: async (_ctx) => {
|
|
2471
2457
|
const google = config.google;
|
|
2472
2458
|
if (!google) {
|
|
2473
|
-
return (0,
|
|
2474
|
-
|
|
2459
|
+
return (0, import_core9.setApiResponse)(
|
|
2460
|
+
import_core9.HTTP.NOT_IMPLEMENTED,
|
|
2475
2461
|
"NOT_CONFIGURED",
|
|
2476
2462
|
"Google OAuth not configured"
|
|
2477
2463
|
);
|
|
@@ -2505,8 +2491,8 @@ function oauthController(store, config) {
|
|
|
2505
2491
|
googleCallback: async (ctx) => {
|
|
2506
2492
|
const google = config.google;
|
|
2507
2493
|
if (!google) {
|
|
2508
|
-
return (0,
|
|
2509
|
-
|
|
2494
|
+
return (0, import_core9.setApiResponse)(
|
|
2495
|
+
import_core9.HTTP.NOT_IMPLEMENTED,
|
|
2510
2496
|
"NOT_CONFIGURED",
|
|
2511
2497
|
"Google OAuth not configured"
|
|
2512
2498
|
);
|
|
@@ -2514,14 +2500,14 @@ function oauthController(store, config) {
|
|
|
2514
2500
|
const url = new URL(ctx.request.url);
|
|
2515
2501
|
const code = url.searchParams.get("code");
|
|
2516
2502
|
if (!code) {
|
|
2517
|
-
return (0,
|
|
2503
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
2518
2504
|
}
|
|
2519
2505
|
const returnedState = url.searchParams.get("state") ?? "";
|
|
2520
2506
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
2521
2507
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
2522
|
-
if (!returnedState || !expectedState || !(0,
|
|
2523
|
-
return (0,
|
|
2524
|
-
|
|
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,
|
|
2525
2511
|
"GOOGLE_AUTH_FAILED",
|
|
2526
2512
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
2527
2513
|
);
|
|
@@ -2539,8 +2525,8 @@ function oauthController(store, config) {
|
|
|
2539
2525
|
});
|
|
2540
2526
|
const tokenData = await tokenRes.json();
|
|
2541
2527
|
if (!tokenData.id_token) {
|
|
2542
|
-
return (0,
|
|
2543
|
-
|
|
2528
|
+
return (0, import_core9.setApiResponse)(
|
|
2529
|
+
import_core9.HTTP.BAD_REQUEST,
|
|
2544
2530
|
"GOOGLE_AUTH_FAILED",
|
|
2545
2531
|
"OAuth token exchange failed"
|
|
2546
2532
|
);
|
|
@@ -2549,32 +2535,32 @@ function oauthController(store, config) {
|
|
|
2549
2535
|
Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
|
|
2550
2536
|
);
|
|
2551
2537
|
if (payload.aud !== google.clientId) {
|
|
2552
|
-
return (0,
|
|
2538
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
|
|
2553
2539
|
}
|
|
2554
2540
|
if (payload.iss !== "https://accounts.google.com" && payload.iss !== "accounts.google.com") {
|
|
2555
|
-
return (0,
|
|
2541
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
|
|
2556
2542
|
}
|
|
2557
2543
|
if (typeof payload.exp !== "number" || payload.exp * 1e3 < Date.now()) {
|
|
2558
|
-
return (0,
|
|
2544
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
|
|
2559
2545
|
}
|
|
2560
2546
|
if (!payload.email) {
|
|
2561
|
-
return (0,
|
|
2547
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
|
|
2562
2548
|
}
|
|
2563
2549
|
if (payload.email_verified !== true) {
|
|
2564
|
-
return (0,
|
|
2565
|
-
|
|
2550
|
+
return (0, import_core9.setApiResponse)(
|
|
2551
|
+
import_core9.HTTP.BAD_REQUEST,
|
|
2566
2552
|
"GOOGLE_AUTH_FAILED",
|
|
2567
2553
|
"Google account email is not verified"
|
|
2568
2554
|
);
|
|
2569
2555
|
}
|
|
2570
2556
|
const normalizedEmail = normalizeEmailSafe(payload.email);
|
|
2571
2557
|
if (!normalizedEmail) {
|
|
2572
|
-
return (0,
|
|
2558
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "Invalid email in OAuth response");
|
|
2573
2559
|
}
|
|
2574
2560
|
const meta = requestMeta(ctx);
|
|
2575
2561
|
const upserted = await users.upsertByProvider(normalizedEmail, "google", payload.sub ?? "");
|
|
2576
2562
|
if (!upserted) {
|
|
2577
|
-
loginEvents.recordSafe({
|
|
2563
|
+
await loginEvents.recordSafe({
|
|
2578
2564
|
userId: null,
|
|
2579
2565
|
emailAttempted: normalizedEmail,
|
|
2580
2566
|
method: "oauth-google",
|
|
@@ -2582,17 +2568,17 @@ function oauthController(store, config) {
|
|
|
2582
2568
|
failureReason: "provider_upsert_failed",
|
|
2583
2569
|
...meta
|
|
2584
2570
|
});
|
|
2585
|
-
return (0,
|
|
2571
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
2586
2572
|
}
|
|
2587
2573
|
const fullUser = await users.findById(upserted.id);
|
|
2588
2574
|
if (!fullUser) {
|
|
2589
|
-
return (0,
|
|
2575
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
2590
2576
|
}
|
|
2591
2577
|
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {
|
|
2592
2578
|
loginMethod: "google"
|
|
2593
2579
|
});
|
|
2594
2580
|
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
2595
|
-
loginEvents.recordSafe({
|
|
2581
|
+
await loginEvents.recordSafe({
|
|
2596
2582
|
userId: upserted.id,
|
|
2597
2583
|
emailAttempted: normalizedEmail,
|
|
2598
2584
|
method: "oauth-google",
|
|
@@ -2621,7 +2607,7 @@ function oauthController(store, config) {
|
|
|
2621
2607
|
appleInit: async (_ctx) => {
|
|
2622
2608
|
const apple = config.apple;
|
|
2623
2609
|
if (!apple) {
|
|
2624
|
-
return (0,
|
|
2610
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
2625
2611
|
}
|
|
2626
2612
|
const state = (0, import_node_crypto7.randomBytes)(16).toString("hex");
|
|
2627
2613
|
const params = new URLSearchParams({
|
|
@@ -2652,20 +2638,20 @@ function oauthController(store, config) {
|
|
|
2652
2638
|
appleCallback: async (ctx) => {
|
|
2653
2639
|
const apple = config.apple;
|
|
2654
2640
|
if (!apple) {
|
|
2655
|
-
return (0,
|
|
2641
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
2656
2642
|
}
|
|
2657
2643
|
const pre = ctx.meta["body"];
|
|
2658
2644
|
const form = pre && typeof pre === "object" ? new URLSearchParams(pre) : new URLSearchParams(await ctx.request.text());
|
|
2659
2645
|
const code = form.get("code");
|
|
2660
2646
|
if (!code) {
|
|
2661
|
-
return (0,
|
|
2647
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
2662
2648
|
}
|
|
2663
2649
|
const returnedState = form.get("state") ?? "";
|
|
2664
2650
|
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
2665
2651
|
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
2666
|
-
if (!returnedState || !expectedState || !(0,
|
|
2667
|
-
return (0,
|
|
2668
|
-
|
|
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,
|
|
2669
2655
|
"APPLE_AUTH_FAILED",
|
|
2670
2656
|
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
2671
2657
|
);
|
|
@@ -2683,11 +2669,11 @@ function oauthController(store, config) {
|
|
|
2683
2669
|
});
|
|
2684
2670
|
const tokenData = await tokenRes.json();
|
|
2685
2671
|
if (!tokenData.id_token) {
|
|
2686
|
-
return (0,
|
|
2672
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
|
|
2687
2673
|
}
|
|
2688
2674
|
const claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });
|
|
2689
2675
|
if (!claims) {
|
|
2690
|
-
return (0,
|
|
2676
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
2691
2677
|
}
|
|
2692
2678
|
return completeAppleLogin(ctx, {
|
|
2693
2679
|
email: claims.email,
|
|
@@ -2698,18 +2684,18 @@ function oauthController(store, config) {
|
|
|
2698
2684
|
appleNative: async (ctx) => {
|
|
2699
2685
|
const apple = config.apple;
|
|
2700
2686
|
if (!apple) {
|
|
2701
|
-
return (0,
|
|
2687
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
2702
2688
|
}
|
|
2703
2689
|
const body = ctx.meta["body"];
|
|
2704
2690
|
const identityToken = body?.identityToken;
|
|
2705
2691
|
if (!identityToken) {
|
|
2706
|
-
return (0,
|
|
2692
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
|
|
2707
2693
|
}
|
|
2708
2694
|
const audiences = apple.nativeClientIds && apple.nativeClientIds.length ? apple.nativeClientIds : [apple.clientId];
|
|
2709
2695
|
const claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });
|
|
2710
2696
|
if (!claims) {
|
|
2711
2697
|
const meta = requestMeta(ctx);
|
|
2712
|
-
loginEvents.recordSafe({
|
|
2698
|
+
await loginEvents.recordSafe({
|
|
2713
2699
|
userId: null,
|
|
2714
2700
|
emailAttempted: null,
|
|
2715
2701
|
method: "oauth-apple",
|
|
@@ -2717,14 +2703,14 @@ function oauthController(store, config) {
|
|
|
2717
2703
|
failureReason: "invalid_identity_token",
|
|
2718
2704
|
...meta
|
|
2719
2705
|
});
|
|
2720
|
-
return (0,
|
|
2706
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
2721
2707
|
}
|
|
2722
2708
|
const tokenHash = (0, import_node_crypto7.createHash)("sha256").update(identityToken).digest("hex");
|
|
2723
2709
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
2724
2710
|
const firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));
|
|
2725
2711
|
if (!firstUse) {
|
|
2726
2712
|
const meta = requestMeta(ctx);
|
|
2727
|
-
loginEvents.recordSafe({
|
|
2713
|
+
await loginEvents.recordSafe({
|
|
2728
2714
|
userId: null,
|
|
2729
2715
|
emailAttempted: claims.email ?? null,
|
|
2730
2716
|
method: "oauth-apple",
|
|
@@ -2732,8 +2718,8 @@ function oauthController(store, config) {
|
|
|
2732
2718
|
failureReason: "token_replayed",
|
|
2733
2719
|
...meta
|
|
2734
2720
|
});
|
|
2735
|
-
return (0,
|
|
2736
|
-
|
|
2721
|
+
return (0, import_core9.setApiResponse)(
|
|
2722
|
+
import_core9.HTTP.UNAUTHORIZED,
|
|
2737
2723
|
"APPLE_AUTH_FAILED",
|
|
2738
2724
|
"This Apple identity token has already been used"
|
|
2739
2725
|
);
|
|
@@ -2831,17 +2817,17 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
2831
2817
|
}
|
|
2832
2818
|
|
|
2833
2819
|
// src/services/config-guard.ts
|
|
2834
|
-
var
|
|
2820
|
+
var import_core10 = require("@fonderie/core");
|
|
2835
2821
|
var MODULE = "@fonderie/auth";
|
|
2836
2822
|
function collectAuthConfigProblems(config) {
|
|
2837
2823
|
const problems = [];
|
|
2838
2824
|
const secret = config.jwtSecret ?? "";
|
|
2839
|
-
const secretProblem = (0,
|
|
2825
|
+
const secretProblem = (0, import_core10.secretStrengthProblem)(secret);
|
|
2840
2826
|
if (secretProblem === "too-short") {
|
|
2841
2827
|
problems.push({
|
|
2842
2828
|
module: MODULE,
|
|
2843
2829
|
severity: "error",
|
|
2844
|
-
message: `jwtSecret must be at least ${
|
|
2830
|
+
message: `jwtSecret must be at least ${import_core10.MIN_SECRET_LENGTH} characters (got ${secret.length})`
|
|
2845
2831
|
});
|
|
2846
2832
|
} else if (secretProblem === "placeholder") {
|
|
2847
2833
|
problems.push({
|
|
@@ -2865,7 +2851,7 @@ function collectAuthConfigProblems(config) {
|
|
|
2865
2851
|
severity: "error",
|
|
2866
2852
|
message: "google OAuth is configured but clientId, clientSecret, or redirectUri is missing"
|
|
2867
2853
|
});
|
|
2868
|
-
} else if (
|
|
2854
|
+
} else if (import_core10.PLACEHOLDER_SECRET.test(clientSecret)) {
|
|
2869
2855
|
problems.push({
|
|
2870
2856
|
module: MODULE,
|
|
2871
2857
|
severity: "error",
|