@fonderie/auth 5.0.2 → 5.0.3
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/dtos/user.cjs +3 -3
- package/dist/dtos/user.cjs.map +1 -1
- package/dist/dtos/user.js +4 -4
- package/dist/dtos/user.js.map +1 -1
- package/dist/index.cjs +156 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +14 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -255,7 +255,7 @@ function cookieHeaders(cookies) {
|
|
|
255
255
|
|
|
256
256
|
// src/controllers/mfa.controller.ts
|
|
257
257
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
258
|
-
var
|
|
258
|
+
var import_core4 = require("@fonderie/core");
|
|
259
259
|
var import_events = require("@fonderie/events");
|
|
260
260
|
|
|
261
261
|
// src/config.ts
|
|
@@ -332,6 +332,7 @@ function verifyToken(token, config) {
|
|
|
332
332
|
|
|
333
333
|
// src/services/mfa.ts
|
|
334
334
|
var import_node_crypto2 = require("crypto");
|
|
335
|
+
var import_core2 = require("@fonderie/core");
|
|
335
336
|
var STEP = 30;
|
|
336
337
|
var DRIFT = 1;
|
|
337
338
|
var DIGITS = 6;
|
|
@@ -402,16 +403,10 @@ function generateTotpUri(email2, secret, issuer) {
|
|
|
402
403
|
});
|
|
403
404
|
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email2)}?${params}`;
|
|
404
405
|
}
|
|
405
|
-
function safeCodeEqual(a, b) {
|
|
406
|
-
const bufA = Buffer.from(a);
|
|
407
|
-
const bufB = Buffer.from(b);
|
|
408
|
-
if (bufA.length !== bufB.length) return false;
|
|
409
|
-
return (0, import_node_crypto2.timingSafeEqual)(bufA, bufB);
|
|
410
|
-
}
|
|
411
406
|
function verifyTotpToken(token, secret) {
|
|
412
407
|
const t = timeCounter();
|
|
413
408
|
for (let i = -DRIFT; i <= DRIFT; i++) {
|
|
414
|
-
if (
|
|
409
|
+
if ((0, import_core2.constantTimeEqual)(hotp(secret, t + i), token)) {
|
|
415
410
|
return true;
|
|
416
411
|
}
|
|
417
412
|
}
|
|
@@ -460,7 +455,7 @@ function makeMfaCipher(keyHex) {
|
|
|
460
455
|
}
|
|
461
456
|
|
|
462
457
|
// src/dtos/user.ts
|
|
463
|
-
var
|
|
458
|
+
var import_core3 = require("@fonderie/core");
|
|
464
459
|
var DEFAULT_PREFERENCES = {
|
|
465
460
|
locale: "en-US",
|
|
466
461
|
timezone: "UTC",
|
|
@@ -491,14 +486,14 @@ function toUserDTO(user, phoneVerified = false) {
|
|
|
491
486
|
const prefs = user.preferences ?? {};
|
|
492
487
|
const cleaned = cleanPreferences(prefs);
|
|
493
488
|
return {
|
|
494
|
-
id: (0,
|
|
495
|
-
email: (0,
|
|
496
|
-
firstName: (0,
|
|
497
|
-
lastName: (0,
|
|
498
|
-
phone: (0,
|
|
499
|
-
profileImageUrl: (0,
|
|
489
|
+
id: (0, import_core3.stringOrEmpty)(user.id),
|
|
490
|
+
email: (0, import_core3.stringOrEmpty)(user.email),
|
|
491
|
+
firstName: (0, import_core3.stringOrEmpty)(user.firstName),
|
|
492
|
+
lastName: (0, import_core3.stringOrEmpty)(user.lastName),
|
|
493
|
+
phone: (0, import_core3.stringOrEmpty)(user.phone),
|
|
494
|
+
profileImageUrl: (0, import_core3.stringOrEmpty)(user.profileImageUrl),
|
|
500
495
|
isActive: typeof user.isActive === "boolean" ? user.isActive : true,
|
|
501
|
-
lastLogin:
|
|
496
|
+
lastLogin: (0, import_core3.dateOrEmpty)(user.lastLogin),
|
|
502
497
|
preferences: {
|
|
503
498
|
...DEFAULT_PREFERENCES,
|
|
504
499
|
...cleaned,
|
|
@@ -511,12 +506,12 @@ function toUserDTO(user, phoneVerified = false) {
|
|
|
511
506
|
},
|
|
512
507
|
isEmailVerified: user.emailVerifiedAt !== null,
|
|
513
508
|
isPhoneVerified: phoneVerified,
|
|
514
|
-
mfaEnabled: (0,
|
|
515
|
-
suspended: (0,
|
|
516
|
-
whitelist: (0,
|
|
509
|
+
mfaEnabled: (0, import_core3.booleanOrFalse)(user.mfaEnabled),
|
|
510
|
+
suspended: (0, import_core3.booleanOrFalse)(user.suspended),
|
|
511
|
+
whitelist: (0, import_core3.booleanOrFalse)(user.whitelist),
|
|
517
512
|
ipWhitelist: Array.isArray(user.ipWhitelist) ? user.ipWhitelist : [],
|
|
518
|
-
createdAt:
|
|
519
|
-
updatedAt:
|
|
513
|
+
createdAt: (0, import_core3.dateOrEmpty)(user.createdAt),
|
|
514
|
+
updatedAt: (0, import_core3.dateOrEmpty)(user.updatedAt)
|
|
520
515
|
};
|
|
521
516
|
}
|
|
522
517
|
|
|
@@ -835,8 +830,8 @@ function mfaController(store, config, issuer, bus) {
|
|
|
835
830
|
users.saveMfaPendingSecret(ctx.user.id, mfaCipher.encrypt(secret)),
|
|
836
831
|
backupCodes.replace(ctx.user.id, codeHashes)
|
|
837
832
|
]);
|
|
838
|
-
return (0,
|
|
839
|
-
|
|
833
|
+
return (0, import_core4.setApiResponse)(
|
|
834
|
+
import_core4.HTTP.OK,
|
|
840
835
|
"MFA_SETUP_INITIATED",
|
|
841
836
|
"Scan the QR code with your authenticator app.",
|
|
842
837
|
{
|
|
@@ -852,13 +847,13 @@ function mfaController(store, config, issuer, bus) {
|
|
|
852
847
|
const body = ctx.meta["body"];
|
|
853
848
|
const token = body?.["token"];
|
|
854
849
|
if (typeof token !== "string") {
|
|
855
|
-
return (0,
|
|
850
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "token is required");
|
|
856
851
|
}
|
|
857
852
|
const pendingCipher = await users.getMfaPendingSecret(ctx.user.id);
|
|
858
853
|
const pendingSecret = pendingCipher ? mfaCipher.decrypt(pendingCipher) : null;
|
|
859
854
|
if (pendingSecret) {
|
|
860
855
|
if (!verifyTotpToken(token, pendingSecret)) {
|
|
861
|
-
return (0,
|
|
856
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid MFA token");
|
|
862
857
|
}
|
|
863
858
|
await users.confirmMfaSecret(ctx.user.id);
|
|
864
859
|
bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
@@ -868,19 +863,19 @@ function mfaController(store, config, issuer, bus) {
|
|
|
868
863
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
869
864
|
}).catch(() => {
|
|
870
865
|
});
|
|
871
|
-
return (0,
|
|
866
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "MFA_VERIFIED", "MFA verified successfully.", {
|
|
872
867
|
mfaEnabled: true
|
|
873
868
|
});
|
|
874
869
|
} else if (/^[A-Z0-9]{8}$/i.test(token)) {
|
|
875
870
|
if (!ctx.user.mfaPending) {
|
|
876
|
-
return (0,
|
|
877
|
-
|
|
871
|
+
return (0, import_core4.setApiResponse)(
|
|
872
|
+
import_core4.HTTP.FORBIDDEN,
|
|
878
873
|
"MFA_NOT_PENDING",
|
|
879
874
|
"Use the mfaToken from the login response"
|
|
880
875
|
);
|
|
881
876
|
}
|
|
882
877
|
if (!ctx.user.mfaEnabled) {
|
|
883
|
-
return (0,
|
|
878
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
|
|
884
879
|
}
|
|
885
880
|
const unused = await backupCodes.findUnused(ctx.user.id);
|
|
886
881
|
const checks = await Promise.all(
|
|
@@ -891,24 +886,24 @@ function mfaController(store, config, issuer, bus) {
|
|
|
891
886
|
);
|
|
892
887
|
const matched = checks.find((r) => r.match);
|
|
893
888
|
if (!matched) {
|
|
894
|
-
return (0,
|
|
889
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid backup code");
|
|
895
890
|
}
|
|
896
891
|
await backupCodes.consume(matched.id);
|
|
897
892
|
} else {
|
|
898
893
|
if (!ctx.user.mfaPending) {
|
|
899
|
-
return (0,
|
|
900
|
-
|
|
894
|
+
return (0, import_core4.setApiResponse)(
|
|
895
|
+
import_core4.HTTP.FORBIDDEN,
|
|
901
896
|
"MFA_NOT_PENDING",
|
|
902
897
|
"Use the mfaToken from the login response"
|
|
903
898
|
);
|
|
904
899
|
}
|
|
905
900
|
const storedSecret = await users.getMfaSecret(ctx.user.id);
|
|
906
901
|
if (!storedSecret) {
|
|
907
|
-
return (0,
|
|
902
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
|
|
908
903
|
}
|
|
909
904
|
const secret = mfaCipher.decrypt(storedSecret);
|
|
910
905
|
if (!verifyTotpToken(token, secret)) {
|
|
911
|
-
return (0,
|
|
906
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid MFA token");
|
|
912
907
|
}
|
|
913
908
|
if (!ctx.user.mfaEnabled) {
|
|
914
909
|
await users.enableMfa(ctx.user.id);
|
|
@@ -921,7 +916,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
921
916
|
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
922
917
|
const fullUser = await users.findById(ctx.user.id);
|
|
923
918
|
if (!fullUser) {
|
|
924
|
-
return (0,
|
|
919
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.SERVER_ERROR, "SERVER_ERROR", "User not found after MFA verify");
|
|
925
920
|
}
|
|
926
921
|
return Response.json(
|
|
927
922
|
{
|
|
@@ -943,15 +938,15 @@ function mfaController(store, config, issuer, bus) {
|
|
|
943
938
|
const body = ctx.meta["body"];
|
|
944
939
|
const token = body?.["token"];
|
|
945
940
|
if (typeof token !== "string") {
|
|
946
|
-
return (0,
|
|
941
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
|
|
947
942
|
}
|
|
948
943
|
if (!ctx.user.mfaEnabled) {
|
|
949
|
-
return (0,
|
|
944
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
|
|
950
945
|
}
|
|
951
946
|
const storedSecret = await users.getMfaSecret(ctx.user.id);
|
|
952
947
|
const secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;
|
|
953
948
|
if (!secret || !verifyTotpToken(token, secret)) {
|
|
954
|
-
return (0,
|
|
949
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
|
|
955
950
|
}
|
|
956
951
|
const plainCodes = generateBackupCodes();
|
|
957
952
|
const codeHashes = await Promise.all(plainCodes.map((c) => hashPassword(c)));
|
|
@@ -963,7 +958,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
963
958
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
964
959
|
}).catch(() => {
|
|
965
960
|
});
|
|
966
|
-
return (0,
|
|
961
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "BACKUP_CODES_REGENERATED", "Backup codes regenerated.", {
|
|
967
962
|
backupCodes: plainCodes
|
|
968
963
|
});
|
|
969
964
|
},
|
|
@@ -972,16 +967,16 @@ function mfaController(store, config, issuer, bus) {
|
|
|
972
967
|
const body = ctx.meta["body"];
|
|
973
968
|
const token = body?.["token"];
|
|
974
969
|
if (typeof token !== "string") {
|
|
975
|
-
return (0,
|
|
970
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
|
|
976
971
|
}
|
|
977
972
|
const user = await users.findById(ctx.user.id);
|
|
978
973
|
if (!user || !user.mfaEnabled) {
|
|
979
|
-
return (0,
|
|
974
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
|
|
980
975
|
}
|
|
981
976
|
const storedSecret = await users.getMfaSecret(ctx.user.id);
|
|
982
977
|
const secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;
|
|
983
978
|
if (!secret || !verifyTotpToken(token, secret)) {
|
|
984
|
-
return (0,
|
|
979
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
|
|
985
980
|
}
|
|
986
981
|
await Promise.all([users.disableMfa(ctx.user.id), backupCodes.deleteByUser(ctx.user.id)]);
|
|
987
982
|
bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
@@ -991,7 +986,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
991
986
|
recipient: { email: user.email, phone: null, deviceToken: null }
|
|
992
987
|
}).catch(() => {
|
|
993
988
|
});
|
|
994
|
-
return (0,
|
|
989
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "MFA_DISABLED", "MFA disabled successfully.");
|
|
995
990
|
}
|
|
996
991
|
};
|
|
997
992
|
}
|
|
@@ -999,7 +994,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
999
994
|
// src/controllers/auth.controller.ts
|
|
1000
995
|
var import_node_crypto4 = require("crypto");
|
|
1001
996
|
var import_events2 = require("@fonderie/events");
|
|
1002
|
-
var
|
|
997
|
+
var import_core5 = require("@fonderie/core");
|
|
1003
998
|
|
|
1004
999
|
// src/services/cooldown.ts
|
|
1005
1000
|
function checkCooldown(lastSentAt, cooldownMs) {
|
|
@@ -1198,18 +1193,18 @@ function authController(store, config, bus) {
|
|
|
1198
1193
|
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
1199
1194
|
const normalizedEmail = normalizeEmailSafe(email2);
|
|
1200
1195
|
if (!normalizedEmail) {
|
|
1201
|
-
return (0,
|
|
1196
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1202
1197
|
}
|
|
1203
1198
|
if (password2.length < 8) {
|
|
1204
|
-
return (0,
|
|
1205
|
-
|
|
1199
|
+
return (0, import_core5.setApiResponse)(
|
|
1200
|
+
import_core5.HTTP.UNPROCESSABLE,
|
|
1206
1201
|
"INVALID_PARAMETER",
|
|
1207
1202
|
"password must be at least 8 characters"
|
|
1208
1203
|
);
|
|
1209
1204
|
}
|
|
1210
1205
|
const existing = await users.findByEmail(normalizedEmail);
|
|
1211
1206
|
if (existing) {
|
|
1212
|
-
return (0,
|
|
1207
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
1213
1208
|
}
|
|
1214
1209
|
const passwordHash = await hashPassword(password2);
|
|
1215
1210
|
const row = await users.create(
|
|
@@ -1219,14 +1214,14 @@ function authController(store, config, bus) {
|
|
|
1219
1214
|
lastName
|
|
1220
1215
|
);
|
|
1221
1216
|
if (!row) {
|
|
1222
|
-
return (0,
|
|
1217
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1223
1218
|
}
|
|
1224
1219
|
const pin = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
|
|
1225
1220
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1226
1221
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1227
1222
|
const user = await users.findById(row.id);
|
|
1228
1223
|
if (!user) {
|
|
1229
|
-
return (0,
|
|
1224
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1230
1225
|
}
|
|
1231
1226
|
const reqId = ctx.meta["requestId"];
|
|
1232
1227
|
const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
|
|
@@ -1278,7 +1273,7 @@ function authController(store, config, bus) {
|
|
|
1278
1273
|
if (isValidPhone(phone2)) {
|
|
1279
1274
|
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
1280
1275
|
if (existing) {
|
|
1281
|
-
return (0,
|
|
1276
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
1282
1277
|
}
|
|
1283
1278
|
const { id } = await users.findOrCreateByPhone(
|
|
1284
1279
|
normalizePhone(phone2),
|
|
@@ -1290,7 +1285,7 @@ function authController(store, config, bus) {
|
|
|
1290
1285
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1291
1286
|
const user = await users.findById(id);
|
|
1292
1287
|
if (!user) {
|
|
1293
|
-
return (0,
|
|
1288
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1294
1289
|
}
|
|
1295
1290
|
const reqId2 = ctx.meta["requestId"];
|
|
1296
1291
|
const reqOpts2 = reqId2 !== void 0 ? { requestId: reqId2 } : void 0;
|
|
@@ -1336,8 +1331,8 @@ function authController(store, config, bus) {
|
|
|
1336
1331
|
}
|
|
1337
1332
|
);
|
|
1338
1333
|
}
|
|
1339
|
-
return (0,
|
|
1340
|
-
|
|
1334
|
+
return (0, import_core5.setApiResponse)(
|
|
1335
|
+
import_core5.HTTP.UNPROCESSABLE,
|
|
1341
1336
|
"INVALID_PARAMETER",
|
|
1342
1337
|
"Provide email + password or a valid phone number"
|
|
1343
1338
|
);
|
|
@@ -1348,11 +1343,11 @@ function authController(store, config, bus) {
|
|
|
1348
1343
|
const { email: rawEmail, password: password2 } = body;
|
|
1349
1344
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
1350
1345
|
if (!email2) {
|
|
1351
|
-
return (0,
|
|
1346
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1352
1347
|
}
|
|
1353
1348
|
const user = await users.findByEmail(email2);
|
|
1354
1349
|
if (!user || !user.passwordHash) {
|
|
1355
|
-
return (0,
|
|
1350
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1356
1351
|
}
|
|
1357
1352
|
const { valid, needsRehash } = await verifyPasswordForLogin(
|
|
1358
1353
|
password2,
|
|
@@ -1360,21 +1355,21 @@ function authController(store, config, bus) {
|
|
|
1360
1355
|
config.legacyVerify
|
|
1361
1356
|
);
|
|
1362
1357
|
if (!valid) {
|
|
1363
|
-
return (0,
|
|
1358
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1364
1359
|
}
|
|
1365
1360
|
if (needsRehash) {
|
|
1366
1361
|
await users.updatePassword(user.id, await hashPassword(password2));
|
|
1367
1362
|
}
|
|
1368
1363
|
if (user.suspended) {
|
|
1369
|
-
return (0,
|
|
1370
|
-
|
|
1364
|
+
return (0, import_core5.setApiResponse)(
|
|
1365
|
+
import_core5.HTTP.FORBIDDEN,
|
|
1371
1366
|
"ACCOUNT_SUSPENDED",
|
|
1372
1367
|
"Account suspended. Please contact support."
|
|
1373
1368
|
);
|
|
1374
1369
|
}
|
|
1375
1370
|
if (user.mfaEnabled) {
|
|
1376
1371
|
const mfaToken = issueMfaPendingToken(user.id, config, "email");
|
|
1377
|
-
return (0,
|
|
1372
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
|
|
1378
1373
|
mfaToken
|
|
1379
1374
|
});
|
|
1380
1375
|
}
|
|
@@ -1404,11 +1399,11 @@ function authController(store, config, bus) {
|
|
|
1404
1399
|
if (isValidPhone(phone2)) {
|
|
1405
1400
|
const user = await users.findByPhone(normalizePhone(phone2));
|
|
1406
1401
|
if (!user) {
|
|
1407
|
-
return (0,
|
|
1402
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1408
1403
|
}
|
|
1409
1404
|
if (user.suspended) {
|
|
1410
|
-
return (0,
|
|
1411
|
-
|
|
1405
|
+
return (0, import_core5.setApiResponse)(
|
|
1406
|
+
import_core5.HTTP.FORBIDDEN,
|
|
1412
1407
|
"ACCOUNT_SUSPENDED",
|
|
1413
1408
|
"Account suspended. Please contact support."
|
|
1414
1409
|
);
|
|
@@ -1442,8 +1437,8 @@ function authController(store, config, bus) {
|
|
|
1442
1437
|
}
|
|
1443
1438
|
);
|
|
1444
1439
|
}
|
|
1445
|
-
return (0,
|
|
1446
|
-
|
|
1440
|
+
return (0, import_core5.setApiResponse)(
|
|
1441
|
+
import_core5.HTTP.UNPROCESSABLE,
|
|
1447
1442
|
"INVALID_PARAMETER",
|
|
1448
1443
|
"Provide email + password or a valid phone number"
|
|
1449
1444
|
);
|
|
@@ -1464,23 +1459,23 @@ function authController(store, config, bus) {
|
|
|
1464
1459
|
refresh: async (ctx) => {
|
|
1465
1460
|
const token = extractRefreshToken(ctx);
|
|
1466
1461
|
if (!token) {
|
|
1467
|
-
return (0,
|
|
1462
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
|
|
1468
1463
|
}
|
|
1469
1464
|
const payload = verifyToken(token, config);
|
|
1470
1465
|
if (!payload || payload.type !== "refresh") {
|
|
1471
|
-
return (0,
|
|
1466
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
1472
1467
|
}
|
|
1473
1468
|
const valid = await sessions.exists(token);
|
|
1474
1469
|
if (!valid) {
|
|
1475
|
-
return (0,
|
|
1476
|
-
|
|
1470
|
+
return (0, import_core5.setApiResponse)(
|
|
1471
|
+
import_core5.HTTP.UNAUTHORIZED,
|
|
1477
1472
|
"TOKEN_REFRESH_FAILED",
|
|
1478
1473
|
"Session expired or already revoked"
|
|
1479
1474
|
);
|
|
1480
1475
|
}
|
|
1481
1476
|
const user = await users.findById(payload.sub);
|
|
1482
1477
|
if (!user || user.suspended || user.deletedAt) {
|
|
1483
|
-
return (0,
|
|
1478
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1484
1479
|
}
|
|
1485
1480
|
await sessions.delete(token);
|
|
1486
1481
|
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
@@ -1504,16 +1499,16 @@ function authController(store, config, bus) {
|
|
|
1504
1499
|
const body = ctx.meta["body"];
|
|
1505
1500
|
const rawEmail = body?.["email"];
|
|
1506
1501
|
if (typeof rawEmail !== "string") {
|
|
1507
|
-
return (0,
|
|
1502
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
1508
1503
|
}
|
|
1509
1504
|
const email2 = normalizeEmailSafe(rawEmail);
|
|
1510
1505
|
if (!email2) {
|
|
1511
|
-
return (0,
|
|
1506
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1512
1507
|
}
|
|
1513
1508
|
const user = await users.findByEmail(email2);
|
|
1514
1509
|
if (!user) {
|
|
1515
|
-
return (0,
|
|
1516
|
-
|
|
1510
|
+
return (0, import_core5.setApiResponse)(
|
|
1511
|
+
import_core5.HTTP.OK,
|
|
1517
1512
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
1518
1513
|
"Password reset email sent (if account exists)."
|
|
1519
1514
|
);
|
|
@@ -1522,8 +1517,8 @@ function authController(store, config, bus) {
|
|
|
1522
1517
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1523
1518
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
1524
1519
|
if (remaining > 0) {
|
|
1525
|
-
return (0,
|
|
1526
|
-
|
|
1520
|
+
return (0, import_core5.setApiResponse)(
|
|
1521
|
+
import_core5.HTTP.OK,
|
|
1527
1522
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
1528
1523
|
"Password reset email sent (if account exists)."
|
|
1529
1524
|
);
|
|
@@ -1538,8 +1533,8 @@ function authController(store, config, bus) {
|
|
|
1538
1533
|
data: { pin }
|
|
1539
1534
|
}).catch(() => {
|
|
1540
1535
|
});
|
|
1541
|
-
return (0,
|
|
1542
|
-
|
|
1536
|
+
return (0, import_core5.setApiResponse)(
|
|
1537
|
+
import_core5.HTTP.OK,
|
|
1543
1538
|
"PASSWORD_RESET_EMAIL_SENT",
|
|
1544
1539
|
"Password reset email sent (if account exists)."
|
|
1545
1540
|
);
|
|
@@ -1549,22 +1544,22 @@ function authController(store, config, bus) {
|
|
|
1549
1544
|
const raw = body?.["pin"];
|
|
1550
1545
|
const password2 = body?.["password"];
|
|
1551
1546
|
if (typeof raw !== "string" || typeof password2 !== "string") {
|
|
1552
|
-
return (0,
|
|
1553
|
-
|
|
1547
|
+
return (0, import_core5.setApiResponse)(
|
|
1548
|
+
import_core5.HTTP.UNPROCESSABLE,
|
|
1554
1549
|
"INVALID_PARAMETER",
|
|
1555
1550
|
"pin and password are required"
|
|
1556
1551
|
);
|
|
1557
1552
|
}
|
|
1558
1553
|
if (!/^\d{6}$/.test(raw.trim())) {
|
|
1559
|
-
return (0,
|
|
1560
|
-
|
|
1554
|
+
return (0, import_core5.setApiResponse)(
|
|
1555
|
+
import_core5.HTTP.UNPROCESSABLE,
|
|
1561
1556
|
"INVALID_PARAMETER",
|
|
1562
1557
|
"pin must be a 6-digit code"
|
|
1563
1558
|
);
|
|
1564
1559
|
}
|
|
1565
1560
|
if (password2.length < 8) {
|
|
1566
|
-
return (0,
|
|
1567
|
-
|
|
1561
|
+
return (0, import_core5.setApiResponse)(
|
|
1562
|
+
import_core5.HTTP.UNPROCESSABLE,
|
|
1568
1563
|
"INVALID_PARAMETER",
|
|
1569
1564
|
"password must be at least 8 characters"
|
|
1570
1565
|
);
|
|
@@ -1572,7 +1567,7 @@ function authController(store, config, bus) {
|
|
|
1572
1567
|
const pin = raw.trim();
|
|
1573
1568
|
const row = await passwordReset.findByPin(pin);
|
|
1574
1569
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1575
|
-
return (0,
|
|
1570
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "PASSWORD_RESET_FAILED", "Invalid or expired pin");
|
|
1576
1571
|
}
|
|
1577
1572
|
const passwordHash = await hashPassword(password2);
|
|
1578
1573
|
await store.transaction(async (tx) => {
|
|
@@ -1584,11 +1579,11 @@ function authController(store, config, bus) {
|
|
|
1584
1579
|
tx.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [row.userId])
|
|
1585
1580
|
]);
|
|
1586
1581
|
});
|
|
1587
|
-
return (0,
|
|
1582
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
|
|
1588
1583
|
},
|
|
1589
1584
|
verify: async (ctx) => {
|
|
1590
1585
|
if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
|
|
1591
|
-
return (0,
|
|
1586
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
1592
1587
|
verified: true,
|
|
1593
1588
|
email: ctx.user.email
|
|
1594
1589
|
});
|
|
@@ -1596,8 +1591,8 @@ function authController(store, config, bus) {
|
|
|
1596
1591
|
const body = ctx.meta["body"];
|
|
1597
1592
|
const raw = body?.["token"];
|
|
1598
1593
|
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
1599
|
-
return (0,
|
|
1600
|
-
|
|
1594
|
+
return (0, import_core5.setApiResponse)(
|
|
1595
|
+
import_core5.HTTP.UNPROCESSABLE,
|
|
1601
1596
|
"INVALID_PARAMETER",
|
|
1602
1597
|
"token must be a 6-digit code"
|
|
1603
1598
|
);
|
|
@@ -1606,12 +1601,12 @@ function authController(store, config, bus) {
|
|
|
1606
1601
|
if (ctx.user.loginMethod === "phone") {
|
|
1607
1602
|
const record = await phoneVerif.findByUser(ctx.user.id, pin);
|
|
1608
1603
|
if (!record) {
|
|
1609
|
-
return (0,
|
|
1604
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
1610
1605
|
}
|
|
1611
1606
|
if (/* @__PURE__ */ new Date() > record.expiresAt) {
|
|
1612
1607
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
1613
|
-
return (0,
|
|
1614
|
-
|
|
1608
|
+
return (0, import_core5.setApiResponse)(
|
|
1609
|
+
import_core5.HTTP.BAD_REQUEST,
|
|
1615
1610
|
"VERIFICATION_FAILED",
|
|
1616
1611
|
"Verification code expired"
|
|
1617
1612
|
);
|
|
@@ -1624,11 +1619,11 @@ function authController(store, config, bus) {
|
|
|
1624
1619
|
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1625
1620
|
const verifiedUser = await users.findById(ctx.user.id);
|
|
1626
1621
|
if (!verifiedUser) {
|
|
1627
|
-
return (0,
|
|
1622
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
1628
1623
|
}
|
|
1629
1624
|
if (verifiedUser.suspended) {
|
|
1630
|
-
return (0,
|
|
1631
|
-
|
|
1625
|
+
return (0, import_core5.setApiResponse)(
|
|
1626
|
+
import_core5.HTTP.FORBIDDEN,
|
|
1632
1627
|
"ACCOUNT_SUSPENDED",
|
|
1633
1628
|
"Account suspended. Please contact support."
|
|
1634
1629
|
);
|
|
@@ -1650,10 +1645,10 @@ function authController(store, config, bus) {
|
|
|
1650
1645
|
}
|
|
1651
1646
|
const row = await emailVerif.findByUser(ctx.user.id, pin);
|
|
1652
1647
|
if (!row) {
|
|
1653
|
-
return (0,
|
|
1648
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
|
|
1654
1649
|
}
|
|
1655
1650
|
if (/* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1656
|
-
return (0,
|
|
1651
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
|
|
1657
1652
|
}
|
|
1658
1653
|
await store.transaction(async (tx) => {
|
|
1659
1654
|
await Promise.all([
|
|
@@ -1667,7 +1662,7 @@ function authController(store, config, bus) {
|
|
|
1667
1662
|
])
|
|
1668
1663
|
]);
|
|
1669
1664
|
});
|
|
1670
|
-
return (0,
|
|
1665
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
1671
1666
|
verified: true,
|
|
1672
1667
|
email: ctx.user.email
|
|
1673
1668
|
});
|
|
@@ -1678,16 +1673,16 @@ function authController(store, config, bus) {
|
|
|
1678
1673
|
if (ctx.user.loginMethod === "phone") {
|
|
1679
1674
|
const phone2 = ctx.user.phone;
|
|
1680
1675
|
if (!phone2) {
|
|
1681
|
-
return (0,
|
|
1682
|
-
|
|
1676
|
+
return (0, import_core5.setApiResponse)(
|
|
1677
|
+
import_core5.HTTP.BAD_REQUEST,
|
|
1683
1678
|
"NO_PHONE_ON_ACCOUNT",
|
|
1684
1679
|
"No phone number associated with this account"
|
|
1685
1680
|
);
|
|
1686
1681
|
}
|
|
1687
1682
|
const remaining2 = checkCooldown(await phoneVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
1688
1683
|
if (remaining2 > 0) {
|
|
1689
|
-
return (0,
|
|
1690
|
-
|
|
1684
|
+
return (0, import_core5.setApiResponse)(
|
|
1685
|
+
import_core5.HTTP.TOO_MANY_REQUESTS,
|
|
1691
1686
|
"VERIFICATION_COOLDOWN",
|
|
1692
1687
|
"Please wait before requesting a new code.",
|
|
1693
1688
|
{
|
|
@@ -1705,29 +1700,29 @@ function authController(store, config, bus) {
|
|
|
1705
1700
|
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
1706
1701
|
}).catch(() => {
|
|
1707
1702
|
});
|
|
1708
|
-
return (0,
|
|
1709
|
-
|
|
1703
|
+
return (0, import_core5.setApiResponse)(
|
|
1704
|
+
import_core5.HTTP.OK,
|
|
1710
1705
|
"VERIFICATION_SENT",
|
|
1711
1706
|
"A verification code has been sent to your phone."
|
|
1712
1707
|
);
|
|
1713
1708
|
}
|
|
1714
1709
|
if (!ctx.user.email) {
|
|
1715
|
-
return (0,
|
|
1716
|
-
|
|
1710
|
+
return (0, import_core5.setApiResponse)(
|
|
1711
|
+
import_core5.HTTP.BAD_REQUEST,
|
|
1717
1712
|
"NO_EMAIL_ON_ACCOUNT",
|
|
1718
1713
|
"No email address associated with this account"
|
|
1719
1714
|
);
|
|
1720
1715
|
}
|
|
1721
1716
|
if (ctx.user.emailVerifiedAt) {
|
|
1722
|
-
return (0,
|
|
1717
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "EMAIL_VERIFIED", "Email already verified.", {
|
|
1723
1718
|
verified: true,
|
|
1724
1719
|
email: ctx.user.email
|
|
1725
1720
|
});
|
|
1726
1721
|
}
|
|
1727
1722
|
const remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user.id), cooldown);
|
|
1728
1723
|
if (remaining > 0) {
|
|
1729
|
-
return (0,
|
|
1730
|
-
|
|
1724
|
+
return (0, import_core5.setApiResponse)(
|
|
1725
|
+
import_core5.HTTP.TOO_MANY_REQUESTS,
|
|
1731
1726
|
"VERIFICATION_COOLDOWN",
|
|
1732
1727
|
"Please wait before requesting a new code.",
|
|
1733
1728
|
{
|
|
@@ -1745,7 +1740,7 @@ function authController(store, config, bus) {
|
|
|
1745
1740
|
data: { pin }
|
|
1746
1741
|
}).catch(() => {
|
|
1747
1742
|
});
|
|
1748
|
-
return (0,
|
|
1743
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
|
|
1749
1744
|
email: ctx.user.email
|
|
1750
1745
|
});
|
|
1751
1746
|
}
|
|
@@ -1754,7 +1749,7 @@ function authController(store, config, bus) {
|
|
|
1754
1749
|
|
|
1755
1750
|
// src/controllers/user.controller.ts
|
|
1756
1751
|
var import_node_crypto5 = require("crypto");
|
|
1757
|
-
var
|
|
1752
|
+
var import_core6 = require("@fonderie/core");
|
|
1758
1753
|
var import_events3 = require("@fonderie/events");
|
|
1759
1754
|
function normalizePhone2(phone2) {
|
|
1760
1755
|
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
@@ -1771,9 +1766,9 @@ function userController(store, config, bus) {
|
|
|
1771
1766
|
me: async (ctx) => {
|
|
1772
1767
|
const user = await users.findById(ctx.user.id);
|
|
1773
1768
|
if (!user) {
|
|
1774
|
-
return (0,
|
|
1769
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
1775
1770
|
}
|
|
1776
|
-
return (0,
|
|
1771
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
|
|
1777
1772
|
user: toUserDTO(user, ctx.user.phoneVerified)
|
|
1778
1773
|
});
|
|
1779
1774
|
},
|
|
@@ -1785,18 +1780,18 @@ function userController(store, config, bus) {
|
|
|
1785
1780
|
if (body?.[key] !== void 0) fields[key] = body[key];
|
|
1786
1781
|
}
|
|
1787
1782
|
if (Object.keys(fields).length === 0) {
|
|
1788
|
-
return (0,
|
|
1789
|
-
|
|
1783
|
+
return (0, import_core6.setApiResponse)(
|
|
1784
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1790
1785
|
"INVALID_PARAMETER",
|
|
1791
1786
|
"Provide at least one of: firstName, lastName, avatarUrl"
|
|
1792
1787
|
);
|
|
1793
1788
|
}
|
|
1794
1789
|
const row = await users.update(ctx.user.id, fields);
|
|
1795
1790
|
if (!row) {
|
|
1796
|
-
return (0,
|
|
1791
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
1797
1792
|
}
|
|
1798
1793
|
const updated = await users.findById(ctx.user.id);
|
|
1799
|
-
return (0,
|
|
1794
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PROFILE_UPDATED", "Profile updated.", {
|
|
1800
1795
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
1801
1796
|
});
|
|
1802
1797
|
},
|
|
@@ -1813,14 +1808,14 @@ function userController(store, config, bus) {
|
|
|
1813
1808
|
if (Object.keys(patch).length > 0) fields.patch = patch;
|
|
1814
1809
|
const row = await users.updatePreferences(ctx.user.id, fields);
|
|
1815
1810
|
if (!row) {
|
|
1816
|
-
return (0,
|
|
1817
|
-
|
|
1811
|
+
return (0, import_core6.setApiResponse)(
|
|
1812
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1818
1813
|
"INVALID_PARAMETER",
|
|
1819
1814
|
"Provide at least one preference field"
|
|
1820
1815
|
);
|
|
1821
1816
|
}
|
|
1822
1817
|
const updated = await users.findById(ctx.user.id);
|
|
1823
|
-
return (0,
|
|
1818
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
|
|
1824
1819
|
user: toUserDTO(updated, ctx.user.phoneVerified)
|
|
1825
1820
|
});
|
|
1826
1821
|
},
|
|
@@ -1829,23 +1824,23 @@ function userController(store, config, bus) {
|
|
|
1829
1824
|
const newEmail = body?.["email"];
|
|
1830
1825
|
const normalised = typeof newEmail === "string" ? normalizeEmailSafe(newEmail) : null;
|
|
1831
1826
|
if (!normalised) {
|
|
1832
|
-
return (0,
|
|
1833
|
-
|
|
1827
|
+
return (0, import_core6.setApiResponse)(
|
|
1828
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1834
1829
|
"INVALID_PARAMETER",
|
|
1835
1830
|
"A valid email address is required"
|
|
1836
1831
|
);
|
|
1837
1832
|
}
|
|
1838
1833
|
const oldEmail = ctx.user.email;
|
|
1839
1834
|
if (normalised === oldEmail) {
|
|
1840
|
-
return (0,
|
|
1841
|
-
|
|
1835
|
+
return (0, import_core6.setApiResponse)(
|
|
1836
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1842
1837
|
"INVALID_PARAMETER",
|
|
1843
1838
|
"New email must differ from current email"
|
|
1844
1839
|
);
|
|
1845
1840
|
}
|
|
1846
1841
|
const existing = await users.findByEmail(normalised);
|
|
1847
1842
|
if (existing) {
|
|
1848
|
-
return (0,
|
|
1843
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
1849
1844
|
}
|
|
1850
1845
|
const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1851
1846
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
@@ -1867,8 +1862,8 @@ function userController(store, config, bus) {
|
|
|
1867
1862
|
}).catch(() => {
|
|
1868
1863
|
});
|
|
1869
1864
|
}
|
|
1870
|
-
return (0,
|
|
1871
|
-
|
|
1865
|
+
return (0, import_core6.setApiResponse)(
|
|
1866
|
+
import_core6.HTTP.OK,
|
|
1872
1867
|
"EMAIL_UPDATED",
|
|
1873
1868
|
"Email updated. A verification code has been sent to your new address.",
|
|
1874
1869
|
{
|
|
@@ -1880,8 +1875,8 @@ function userController(store, config, bus) {
|
|
|
1880
1875
|
const body = ctx.meta["body"];
|
|
1881
1876
|
const newPhone = body?.["phone"];
|
|
1882
1877
|
if (!isValidPhone2(newPhone)) {
|
|
1883
|
-
return (0,
|
|
1884
|
-
|
|
1878
|
+
return (0, import_core6.setApiResponse)(
|
|
1879
|
+
import_core6.HTTP.UNPROCESSABLE,
|
|
1885
1880
|
"INVALID_PARAMETER",
|
|
1886
1881
|
"A valid phone number is required"
|
|
1887
1882
|
);
|
|
@@ -1889,7 +1884,7 @@ function userController(store, config, bus) {
|
|
|
1889
1884
|
const normalised = normalizePhone2(newPhone);
|
|
1890
1885
|
const existing = await users.findByPhone(normalised);
|
|
1891
1886
|
if (existing) {
|
|
1892
|
-
return (0,
|
|
1887
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
1893
1888
|
}
|
|
1894
1889
|
const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
|
|
1895
1890
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
@@ -1911,8 +1906,8 @@ function userController(store, config, bus) {
|
|
|
1911
1906
|
}).catch(() => {
|
|
1912
1907
|
});
|
|
1913
1908
|
}
|
|
1914
|
-
return (0,
|
|
1915
|
-
|
|
1909
|
+
return (0, import_core6.setApiResponse)(
|
|
1910
|
+
import_core6.HTTP.OK,
|
|
1916
1911
|
"PHONE_UPDATED",
|
|
1917
1912
|
"Phone number updated. A verification code has been sent to your new number.",
|
|
1918
1913
|
{
|
|
@@ -1925,22 +1920,22 @@ function userController(store, config, bus) {
|
|
|
1925
1920
|
const currentPassword = body?.["currentPassword"];
|
|
1926
1921
|
const newPassword = body?.["newPassword"];
|
|
1927
1922
|
if (typeof currentPassword !== "string" || !currentPassword) {
|
|
1928
|
-
return (0,
|
|
1923
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
|
|
1929
1924
|
}
|
|
1930
1925
|
if (typeof newPassword !== "string" || newPassword.length < 8) {
|
|
1931
|
-
return (0,
|
|
1926
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
|
|
1932
1927
|
}
|
|
1933
1928
|
const user = await users.findById(ctx.user.id);
|
|
1934
|
-
if (!user) return (0,
|
|
1929
|
+
if (!user) return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
1935
1930
|
const { hashPassword: hashPassword2, verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password(), password_exports));
|
|
1936
1931
|
const valid = await verifyPassword2(currentPassword, user.passwordHash ?? "");
|
|
1937
1932
|
if (!valid) {
|
|
1938
|
-
return (0,
|
|
1933
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
|
|
1939
1934
|
}
|
|
1940
1935
|
const hash = await hashPassword2(newPassword);
|
|
1941
1936
|
await users.updatePassword(ctx.user.id, hash);
|
|
1942
1937
|
await sessions.deleteByUser(ctx.user.id);
|
|
1943
|
-
return (0,
|
|
1938
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PASSWORD_CHANGED", "Password updated successfully.");
|
|
1944
1939
|
},
|
|
1945
1940
|
deleteMe: async (ctx) => {
|
|
1946
1941
|
const userId = ctx.user.id;
|
|
@@ -1965,7 +1960,7 @@ function userController(store, config, bus) {
|
|
|
1965
1960
|
// no MFA secret, no session tokens (session metadata only).
|
|
1966
1961
|
exportMe: async (ctx) => {
|
|
1967
1962
|
const user = await users.findById(ctx.user.id);
|
|
1968
|
-
if (!user) return (0,
|
|
1963
|
+
if (!user) return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
1969
1964
|
const sessionRows = await sessions.listByUser(ctx.user.id);
|
|
1970
1965
|
const bundle = {
|
|
1971
1966
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1974,8 +1969,8 @@ function userController(store, config, bus) {
|
|
|
1974
1969
|
id: s.id,
|
|
1975
1970
|
userAgent: s.userAgent,
|
|
1976
1971
|
ipAddress: s.ipAddress,
|
|
1977
|
-
createdAt:
|
|
1978
|
-
expiresAt:
|
|
1972
|
+
createdAt: (0, import_core6.dateOrEmpty)(s.createdAt),
|
|
1973
|
+
expiresAt: (0, import_core6.dateOrEmpty)(s.expiresAt)
|
|
1979
1974
|
})),
|
|
1980
1975
|
security: {
|
|
1981
1976
|
mfaEnabled: user.mfaEnabled === true,
|
|
@@ -1994,13 +1989,13 @@ function userController(store, config, bus) {
|
|
|
1994
1989
|
}
|
|
1995
1990
|
bundle["modules"] = modules;
|
|
1996
1991
|
}
|
|
1997
|
-
return (0,
|
|
1992
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "DATA_EXPORT", "Your account data.", bundle);
|
|
1998
1993
|
}
|
|
1999
1994
|
};
|
|
2000
1995
|
}
|
|
2001
1996
|
|
|
2002
1997
|
// src/controllers/oauth.controller.ts
|
|
2003
|
-
var
|
|
1998
|
+
var import_core7 = require("@fonderie/core");
|
|
2004
1999
|
function oauthController(store, config) {
|
|
2005
2000
|
const users = new UserModel(store);
|
|
2006
2001
|
const sessions = new SessionModel(store);
|
|
@@ -2008,8 +2003,8 @@ function oauthController(store, config) {
|
|
|
2008
2003
|
googleInit: async (_ctx) => {
|
|
2009
2004
|
const google = config.google;
|
|
2010
2005
|
if (!google) {
|
|
2011
|
-
return (0,
|
|
2012
|
-
|
|
2006
|
+
return (0, import_core7.setApiResponse)(
|
|
2007
|
+
import_core7.HTTP.NOT_IMPLEMENTED,
|
|
2013
2008
|
"NOT_CONFIGURED",
|
|
2014
2009
|
"Google OAuth not configured"
|
|
2015
2010
|
);
|
|
@@ -2021,8 +2016,8 @@ function oauthController(store, config) {
|
|
|
2021
2016
|
scope: "openid email profile"
|
|
2022
2017
|
});
|
|
2023
2018
|
const url = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
|
|
2024
|
-
return (0,
|
|
2025
|
-
|
|
2019
|
+
return (0, import_core7.setApiResponse)(
|
|
2020
|
+
import_core7.HTTP.OK,
|
|
2026
2021
|
"GOOGLE_AUTH_URL",
|
|
2027
2022
|
"Redirect the user to the returned URL to begin Google OAuth.",
|
|
2028
2023
|
{ url }
|
|
@@ -2031,8 +2026,8 @@ function oauthController(store, config) {
|
|
|
2031
2026
|
googleCallback: async (ctx) => {
|
|
2032
2027
|
const google = config.google;
|
|
2033
2028
|
if (!google) {
|
|
2034
|
-
return (0,
|
|
2035
|
-
|
|
2029
|
+
return (0, import_core7.setApiResponse)(
|
|
2030
|
+
import_core7.HTTP.NOT_IMPLEMENTED,
|
|
2036
2031
|
"NOT_CONFIGURED",
|
|
2037
2032
|
"Google OAuth not configured"
|
|
2038
2033
|
);
|
|
@@ -2040,7 +2035,7 @@ function oauthController(store, config) {
|
|
|
2040
2035
|
const url = new URL(ctx.request.url);
|
|
2041
2036
|
const code = url.searchParams.get("code");
|
|
2042
2037
|
if (!code) {
|
|
2043
|
-
return (0,
|
|
2038
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
2044
2039
|
}
|
|
2045
2040
|
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
|
2046
2041
|
method: "POST",
|
|
@@ -2055,8 +2050,8 @@ function oauthController(store, config) {
|
|
|
2055
2050
|
});
|
|
2056
2051
|
const tokenData = await tokenRes.json();
|
|
2057
2052
|
if (!tokenData.id_token) {
|
|
2058
|
-
return (0,
|
|
2059
|
-
|
|
2053
|
+
return (0, import_core7.setApiResponse)(
|
|
2054
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
2060
2055
|
"GOOGLE_AUTH_FAILED",
|
|
2061
2056
|
"OAuth token exchange failed"
|
|
2062
2057
|
);
|
|
@@ -2065,19 +2060,19 @@ function oauthController(store, config) {
|
|
|
2065
2060
|
Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
|
|
2066
2061
|
);
|
|
2067
2062
|
if (!payload.email) {
|
|
2068
|
-
return (0,
|
|
2063
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
|
|
2069
2064
|
}
|
|
2070
2065
|
const normalizedEmail = normalizeEmailSafe(payload.email);
|
|
2071
2066
|
if (!normalizedEmail) {
|
|
2072
|
-
return (0,
|
|
2067
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "Invalid email in OAuth response");
|
|
2073
2068
|
}
|
|
2074
2069
|
const upserted = await users.upsertByProvider(normalizedEmail, "google", payload.sub ?? "");
|
|
2075
2070
|
if (!upserted) {
|
|
2076
|
-
return (0,
|
|
2071
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
2077
2072
|
}
|
|
2078
2073
|
const fullUser = await users.findById(upserted.id);
|
|
2079
2074
|
if (!fullUser) {
|
|
2080
|
-
return (0,
|
|
2075
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
2081
2076
|
}
|
|
2082
2077
|
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {
|
|
2083
2078
|
loginMethod: "google"
|
|
@@ -2159,19 +2154,19 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
2159
2154
|
}
|
|
2160
2155
|
|
|
2161
2156
|
// src/services/config-guard.ts
|
|
2157
|
+
var import_core8 = require("@fonderie/core");
|
|
2162
2158
|
var MODULE = "@fonderie/auth";
|
|
2163
|
-
var PLACEHOLDER_SECRET = /dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|min-32-chars/i;
|
|
2164
|
-
var MIN_SECRET_LENGTH = 32;
|
|
2165
2159
|
function collectAuthConfigProblems(config) {
|
|
2166
2160
|
const problems = [];
|
|
2167
2161
|
const secret = config.jwtSecret ?? "";
|
|
2168
|
-
|
|
2162
|
+
const secretProblem = (0, import_core8.secretStrengthProblem)(secret);
|
|
2163
|
+
if (secretProblem === "too-short") {
|
|
2169
2164
|
problems.push({
|
|
2170
2165
|
module: MODULE,
|
|
2171
2166
|
severity: "error",
|
|
2172
|
-
message: `jwtSecret must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`
|
|
2167
|
+
message: `jwtSecret must be at least ${import_core8.MIN_SECRET_LENGTH} characters (got ${secret.length})`
|
|
2173
2168
|
});
|
|
2174
|
-
} else if (
|
|
2169
|
+
} else if (secretProblem === "placeholder") {
|
|
2175
2170
|
problems.push({
|
|
2176
2171
|
module: MODULE,
|
|
2177
2172
|
severity: "error",
|
|
@@ -2193,7 +2188,7 @@ function collectAuthConfigProblems(config) {
|
|
|
2193
2188
|
severity: "error",
|
|
2194
2189
|
message: "google OAuth is configured but clientId, clientSecret, or redirectUri is missing"
|
|
2195
2190
|
});
|
|
2196
|
-
} else if (PLACEHOLDER_SECRET.test(clientSecret)) {
|
|
2191
|
+
} else if (import_core8.PLACEHOLDER_SECRET.test(clientSecret)) {
|
|
2197
2192
|
problems.push({
|
|
2198
2193
|
module: MODULE,
|
|
2199
2194
|
severity: "error",
|