@fonderie/auth 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +160 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -2
- package/dist/index.d.ts +81 -2
- package/dist/index.js +148 -77
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -0
- package/package.json +8 -7
- package/dist/migrations/sql/001_auth.sql +0 -73
- package/dist/migrations/sql/002_phone_auth.sql +0 -20
- package/dist/migrations/sql/003_phone_registration_name.sql +0 -4
- package/dist/migrations/sql/004_drop_phone_verif_name_cols.sql +0 -5
- package/dist/migrations/sql/005_phone_verification_user_id.sql +0 -6
- package/dist/migrations/sql/006_drop_phone_verified_at.sql +0 -1
- package/dist/migrations/sql/007_email_verif_user_id_pk.sql +0 -14
- package/dist/migrations/sql/008_password_reset_pin.sql +0 -4
- package/dist/migrations/sql/009_password_reset_created_at.sql +0 -1
- package/dist/migrations/sql/010_mfa_pending_secret.sql +0 -3
- package/dist/migrations/sql/011_mfa_backup_codes.sql +0 -10
- package/dist/migrations/sql/012_drop_skills.sql +0 -1
package/dist/index.js
CHANGED
|
@@ -45,6 +45,74 @@ var requireEmailLogin = async (ctx, next) => {
|
|
|
45
45
|
return next();
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
// src/middlewares/validate.ts
|
|
49
|
+
import { validate } from "@fonderie/core/middlewares";
|
|
50
|
+
|
|
51
|
+
// src/schemas.ts
|
|
52
|
+
var schemas_exports = {};
|
|
53
|
+
__export(schemas_exports, {
|
|
54
|
+
changePasswordSchema: () => changePasswordSchema,
|
|
55
|
+
forgotPasswordSchema: () => forgotPasswordSchema,
|
|
56
|
+
loginSchema: () => loginSchema,
|
|
57
|
+
mfaTokenSchema: () => mfaTokenSchema,
|
|
58
|
+
refreshSchema: () => refreshSchema,
|
|
59
|
+
registerSchema: () => registerSchema,
|
|
60
|
+
resetPasswordSchema: () => resetPasswordSchema,
|
|
61
|
+
updateEmailSchema: () => updateEmailSchema,
|
|
62
|
+
updatePhoneSchema: () => updatePhoneSchema,
|
|
63
|
+
updatePreferencesSchema: () => updatePreferencesSchema,
|
|
64
|
+
updateProfileSchema: () => updateProfileSchema,
|
|
65
|
+
verifySchema: () => verifySchema
|
|
66
|
+
});
|
|
67
|
+
import { z } from "zod";
|
|
68
|
+
var email = z.string().trim().pipe(z.email());
|
|
69
|
+
var password = z.string().min(8, "password must be at least 8 characters").max(128);
|
|
70
|
+
var phone = z.string().refine((v) => /^\+?[1-9]\d{6,14}$/.test(v.replace(/[\s\-()]/g, "")), "Invalid phone number");
|
|
71
|
+
var sixDigitPin = z.string().trim().regex(/^\d{6}$/, "must be a 6-digit code");
|
|
72
|
+
var registerSchema = z.union([
|
|
73
|
+
z.object({
|
|
74
|
+
email,
|
|
75
|
+
password,
|
|
76
|
+
firstName: z.string().max(100).nullish(),
|
|
77
|
+
lastName: z.string().max(100).nullish()
|
|
78
|
+
}),
|
|
79
|
+
z.object({ phone })
|
|
80
|
+
]);
|
|
81
|
+
var loginSchema = z.union([
|
|
82
|
+
z.object({ email, password: z.string().min(1).max(128) }),
|
|
83
|
+
z.object({ phone })
|
|
84
|
+
]);
|
|
85
|
+
var refreshSchema = z.object({ refreshToken: z.string().min(1).optional() });
|
|
86
|
+
var forgotPasswordSchema = z.object({ email });
|
|
87
|
+
var resetPasswordSchema = z.object({ pin: sixDigitPin, password });
|
|
88
|
+
var verifySchema = z.object({ token: sixDigitPin });
|
|
89
|
+
var updateProfileSchema = z.object({
|
|
90
|
+
firstName: z.string().max(100).nullable().optional(),
|
|
91
|
+
lastName: z.string().max(100).nullable().optional(),
|
|
92
|
+
avatarUrl: z.string().trim().pipe(z.url()).nullable().optional()
|
|
93
|
+
}).refine(
|
|
94
|
+
(o) => Object.values(o).some((v) => v !== void 0),
|
|
95
|
+
"Provide at least one of: firstName, lastName, avatarUrl"
|
|
96
|
+
);
|
|
97
|
+
var updatePreferencesSchema = z.object({
|
|
98
|
+
locale: z.string().max(35).optional(),
|
|
99
|
+
timezone: z.string().max(64).optional(),
|
|
100
|
+
notifications: z.unknown().optional(),
|
|
101
|
+
emailDigest: z.unknown().optional(),
|
|
102
|
+
dateFormat: z.unknown().optional(),
|
|
103
|
+
timeFormat: z.unknown().optional()
|
|
104
|
+
}).refine(
|
|
105
|
+
(o) => Object.values(o).some((v) => v !== void 0),
|
|
106
|
+
"Provide at least one preference field"
|
|
107
|
+
);
|
|
108
|
+
var updateEmailSchema = z.object({ email });
|
|
109
|
+
var updatePhoneSchema = z.object({ phone });
|
|
110
|
+
var changePasswordSchema = z.object({
|
|
111
|
+
currentPassword: z.string().min(1).max(128),
|
|
112
|
+
newPassword: password
|
|
113
|
+
});
|
|
114
|
+
var mfaTokenSchema = z.object({ token: z.string().trim().min(6).max(64) });
|
|
115
|
+
|
|
48
116
|
// src/controllers/mfa.controller.ts
|
|
49
117
|
import QRCode from "qrcode";
|
|
50
118
|
import { setApiResponse as setApiResponse2, HTTP as HTTP2 } from "@fonderie/core";
|
|
@@ -181,7 +249,7 @@ function base32Encode(buf) {
|
|
|
181
249
|
function generateTotpSecret() {
|
|
182
250
|
return base32Encode(randomBytes(20));
|
|
183
251
|
}
|
|
184
|
-
function generateTotpUri(
|
|
252
|
+
function generateTotpUri(email2, secret, issuer) {
|
|
185
253
|
const params = new URLSearchParams({
|
|
186
254
|
secret,
|
|
187
255
|
issuer,
|
|
@@ -189,7 +257,7 @@ function generateTotpUri(email, secret, issuer) {
|
|
|
189
257
|
digits: String(DIGITS),
|
|
190
258
|
period: String(STEP)
|
|
191
259
|
});
|
|
192
|
-
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(
|
|
260
|
+
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email2)}?${params}`;
|
|
193
261
|
}
|
|
194
262
|
function verifyTotpToken(token, secret) {
|
|
195
263
|
const t = timeCounter();
|
|
@@ -281,21 +349,21 @@ var UserModel = class {
|
|
|
281
349
|
);
|
|
282
350
|
return row ?? null;
|
|
283
351
|
}
|
|
284
|
-
async findByEmail(
|
|
352
|
+
async findByEmail(email2) {
|
|
285
353
|
const [row] = await this.store.query(
|
|
286
354
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,
|
|
287
|
-
[
|
|
355
|
+
[email2]
|
|
288
356
|
);
|
|
289
357
|
return row ?? null;
|
|
290
358
|
}
|
|
291
|
-
async findByPhone(
|
|
359
|
+
async findByPhone(phone2) {
|
|
292
360
|
const [row] = await this.store.query(
|
|
293
361
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
|
|
294
|
-
[
|
|
362
|
+
[phone2]
|
|
295
363
|
);
|
|
296
364
|
return row ?? null;
|
|
297
365
|
}
|
|
298
|
-
async findOrCreateByPhone(
|
|
366
|
+
async findOrCreateByPhone(phone2, firstName = null, lastName = null) {
|
|
299
367
|
const [row] = await this.store.query(
|
|
300
368
|
`INSERT INTO fonderie_users (phone, first_name, last_name)
|
|
301
369
|
VALUES ($1, $2, $3)
|
|
@@ -304,16 +372,16 @@ var UserModel = class {
|
|
|
304
372
|
last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),
|
|
305
373
|
updated_at = now()
|
|
306
374
|
RETURNING id`,
|
|
307
|
-
[
|
|
375
|
+
[phone2, firstName, lastName]
|
|
308
376
|
);
|
|
309
377
|
return row;
|
|
310
378
|
}
|
|
311
|
-
async create(
|
|
379
|
+
async create(email2, passwordHash, firstName, lastName) {
|
|
312
380
|
const [row] = await this.store.query(
|
|
313
381
|
`INSERT INTO fonderie_users (email, password_hash, first_name, last_name)
|
|
314
382
|
VALUES ($1, $2, $3, $4)
|
|
315
383
|
RETURNING id`,
|
|
316
|
-
[
|
|
384
|
+
[email2.toLowerCase().trim(), passwordHash, firstName, lastName]
|
|
317
385
|
);
|
|
318
386
|
return row ?? null;
|
|
319
387
|
}
|
|
@@ -400,16 +468,16 @@ var UserModel = class {
|
|
|
400
468
|
[id]
|
|
401
469
|
);
|
|
402
470
|
}
|
|
403
|
-
async updateEmail(id,
|
|
471
|
+
async updateEmail(id, email2) {
|
|
404
472
|
await this.store.query(
|
|
405
473
|
`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,
|
|
406
|
-
[
|
|
474
|
+
[email2.toLowerCase().trim(), id]
|
|
407
475
|
);
|
|
408
476
|
}
|
|
409
|
-
async updatePhone(id,
|
|
477
|
+
async updatePhone(id, phone2) {
|
|
410
478
|
await this.store.query(
|
|
411
479
|
`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,
|
|
412
|
-
[
|
|
480
|
+
[phone2, id]
|
|
413
481
|
);
|
|
414
482
|
}
|
|
415
483
|
async updatePreferences(id, fields) {
|
|
@@ -442,14 +510,14 @@ var UserModel = class {
|
|
|
442
510
|
);
|
|
443
511
|
return row?.mfa_secret ?? null;
|
|
444
512
|
}
|
|
445
|
-
async upsertByProvider(
|
|
513
|
+
async upsertByProvider(email2, provider, providerId) {
|
|
446
514
|
const [row] = await this.store.query(
|
|
447
515
|
`INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
|
|
448
516
|
VALUES ($1, now(), $2, $3)
|
|
449
517
|
ON CONFLICT (email) DO UPDATE
|
|
450
518
|
SET provider = $2, provider_id = $3
|
|
451
519
|
RETURNING id`,
|
|
452
|
-
[
|
|
520
|
+
[email2, provider, providerId]
|
|
453
521
|
);
|
|
454
522
|
return row ?? null;
|
|
455
523
|
}
|
|
@@ -706,11 +774,11 @@ function checkCooldown(lastSentAt, cooldownMs) {
|
|
|
706
774
|
init_password();
|
|
707
775
|
|
|
708
776
|
// src/services/email.ts
|
|
709
|
-
function normalizeEmail(
|
|
710
|
-
if (typeof
|
|
777
|
+
function normalizeEmail(email2) {
|
|
778
|
+
if (typeof email2 !== "string" || email2.length === 0) {
|
|
711
779
|
throw new Error("Invalid email");
|
|
712
780
|
}
|
|
713
|
-
const lower =
|
|
781
|
+
const lower = email2.trim().toLowerCase();
|
|
714
782
|
if (lower.length === 0) {
|
|
715
783
|
throw new Error("Email cannot be empty");
|
|
716
784
|
}
|
|
@@ -730,9 +798,9 @@ function normalizeEmail(email) {
|
|
|
730
798
|
}
|
|
731
799
|
return `${normalizedLocal}@${domain}`;
|
|
732
800
|
}
|
|
733
|
-
function normalizeEmailSafe(
|
|
801
|
+
function normalizeEmailSafe(email2) {
|
|
734
802
|
try {
|
|
735
|
-
return normalizeEmail(
|
|
803
|
+
return normalizeEmail(email2);
|
|
736
804
|
} catch {
|
|
737
805
|
return null;
|
|
738
806
|
}
|
|
@@ -833,13 +901,13 @@ var PhoneVerificationModel = class {
|
|
|
833
901
|
this.store = store;
|
|
834
902
|
}
|
|
835
903
|
store;
|
|
836
|
-
async upsert(userId,
|
|
904
|
+
async upsert(userId, phone2, otp, expiresAt) {
|
|
837
905
|
await this.store.query(
|
|
838
906
|
`INSERT INTO fonderie_phone_verifications (phone, user_id, otp, expires_at)
|
|
839
907
|
VALUES ($1, $2, $3, $4)
|
|
840
908
|
ON CONFLICT (phone) DO UPDATE
|
|
841
909
|
SET user_id = $2, otp = $3, expires_at = $4, created_at = now()`,
|
|
842
|
-
[
|
|
910
|
+
[phone2, userId, otp, expiresAt]
|
|
843
911
|
);
|
|
844
912
|
}
|
|
845
913
|
async findByUser(userId, otp) {
|
|
@@ -864,11 +932,11 @@ var PhoneVerificationModel = class {
|
|
|
864
932
|
};
|
|
865
933
|
|
|
866
934
|
// src/controllers/auth.controller.ts
|
|
867
|
-
function normalizePhone(
|
|
868
|
-
return
|
|
935
|
+
function normalizePhone(phone2) {
|
|
936
|
+
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
869
937
|
}
|
|
870
|
-
function isValidPhone(
|
|
871
|
-
return typeof
|
|
938
|
+
function isValidPhone(phone2) {
|
|
939
|
+
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone(phone2));
|
|
872
940
|
}
|
|
873
941
|
function extractRefreshToken(ctx) {
|
|
874
942
|
const body = ctx.meta["body"];
|
|
@@ -889,13 +957,13 @@ function authController(store, config, bus) {
|
|
|
889
957
|
return {
|
|
890
958
|
register: async (ctx) => {
|
|
891
959
|
const body = ctx.meta["body"];
|
|
892
|
-
const { email, password, phone, firstName = null, lastName = null } = body ?? {};
|
|
893
|
-
if (typeof
|
|
894
|
-
const normalizedEmail = normalizeEmailSafe(
|
|
960
|
+
const { email: email2, password: password2, phone: phone2, firstName = null, lastName = null } = body ?? {};
|
|
961
|
+
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
962
|
+
const normalizedEmail = normalizeEmailSafe(email2);
|
|
895
963
|
if (!normalizedEmail) {
|
|
896
964
|
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
897
965
|
}
|
|
898
|
-
if (
|
|
966
|
+
if (password2.length < 8) {
|
|
899
967
|
return setApiResponse3(
|
|
900
968
|
HTTP3.UNPROCESSABLE,
|
|
901
969
|
"INVALID_PARAMETER",
|
|
@@ -906,7 +974,7 @@ function authController(store, config, bus) {
|
|
|
906
974
|
if (existing) {
|
|
907
975
|
return setApiResponse3(HTTP3.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
908
976
|
}
|
|
909
|
-
const passwordHash = await hashPassword(
|
|
977
|
+
const passwordHash = await hashPassword(password2);
|
|
910
978
|
const row = await users.create(
|
|
911
979
|
normalizedEmail,
|
|
912
980
|
passwordHash,
|
|
@@ -974,19 +1042,19 @@ function authController(store, config, bus) {
|
|
|
974
1042
|
}
|
|
975
1043
|
);
|
|
976
1044
|
}
|
|
977
|
-
if (isValidPhone(
|
|
978
|
-
const existing = await users.findByPhone(normalizePhone(
|
|
1045
|
+
if (isValidPhone(phone2)) {
|
|
1046
|
+
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
979
1047
|
if (existing) {
|
|
980
1048
|
return setApiResponse3(HTTP3.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
981
1049
|
}
|
|
982
1050
|
const { id } = await users.findOrCreateByPhone(
|
|
983
|
-
normalizePhone(
|
|
1051
|
+
normalizePhone(phone2),
|
|
984
1052
|
firstName ?? null,
|
|
985
1053
|
lastName ?? null
|
|
986
1054
|
);
|
|
987
1055
|
const otp = randomInt(1e5, 1e6).toString();
|
|
988
1056
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
989
|
-
await phoneVerif.upsert(id, normalizePhone(
|
|
1057
|
+
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
990
1058
|
const user = await users.findById(id);
|
|
991
1059
|
if (!user) {
|
|
992
1060
|
return setApiResponse3(HTTP3.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
@@ -998,7 +1066,7 @@ function authController(store, config, bus) {
|
|
|
998
1066
|
{
|
|
999
1067
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1000
1068
|
data: { otp },
|
|
1001
|
-
recipient: { email: null, phone: normalizePhone(
|
|
1069
|
+
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1002
1070
|
},
|
|
1003
1071
|
reqOpts2
|
|
1004
1072
|
).catch(() => {
|
|
@@ -1048,16 +1116,16 @@ function authController(store, config, bus) {
|
|
|
1048
1116
|
login: async (ctx) => {
|
|
1049
1117
|
const body = ctx.meta["body"];
|
|
1050
1118
|
if (typeof body?.["email"] === "string" && typeof body?.["password"] === "string") {
|
|
1051
|
-
const { email: rawEmail, password } = body;
|
|
1052
|
-
const
|
|
1053
|
-
if (!
|
|
1119
|
+
const { email: rawEmail, password: password2 } = body;
|
|
1120
|
+
const email2 = normalizeEmailSafe(rawEmail);
|
|
1121
|
+
if (!email2) {
|
|
1054
1122
|
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1055
1123
|
}
|
|
1056
|
-
const user = await users.findByEmail(
|
|
1124
|
+
const user = await users.findByEmail(email2);
|
|
1057
1125
|
if (!user || !user.passwordHash) {
|
|
1058
1126
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1059
1127
|
}
|
|
1060
|
-
const valid = await verifyPassword(
|
|
1128
|
+
const valid = await verifyPassword(password2, user.passwordHash);
|
|
1061
1129
|
if (!valid) {
|
|
1062
1130
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1063
1131
|
}
|
|
@@ -1101,9 +1169,9 @@ function authController(store, config, bus) {
|
|
|
1101
1169
|
}
|
|
1102
1170
|
);
|
|
1103
1171
|
}
|
|
1104
|
-
const
|
|
1105
|
-
if (isValidPhone(
|
|
1106
|
-
const user = await users.findByPhone(normalizePhone(
|
|
1172
|
+
const phone2 = body?.["phone"];
|
|
1173
|
+
if (isValidPhone(phone2)) {
|
|
1174
|
+
const user = await users.findByPhone(normalizePhone(phone2));
|
|
1107
1175
|
if (!user) {
|
|
1108
1176
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1109
1177
|
}
|
|
@@ -1116,11 +1184,11 @@ function authController(store, config, bus) {
|
|
|
1116
1184
|
}
|
|
1117
1185
|
const otp = randomInt(1e5, 1e6).toString();
|
|
1118
1186
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1119
|
-
await phoneVerif.upsert(user.id, normalizePhone(
|
|
1187
|
+
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1120
1188
|
bus?.emit(NOTIFICATION_EVENT2, {
|
|
1121
1189
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1122
1190
|
data: { otp },
|
|
1123
|
-
recipient: { email: null, phone: normalizePhone(
|
|
1191
|
+
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1124
1192
|
}).catch(() => {
|
|
1125
1193
|
});
|
|
1126
1194
|
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
@@ -1221,11 +1289,11 @@ function authController(store, config, bus) {
|
|
|
1221
1289
|
if (typeof rawEmail !== "string") {
|
|
1222
1290
|
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
1223
1291
|
}
|
|
1224
|
-
const
|
|
1225
|
-
if (!
|
|
1292
|
+
const email2 = normalizeEmailSafe(rawEmail);
|
|
1293
|
+
if (!email2) {
|
|
1226
1294
|
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1227
1295
|
}
|
|
1228
|
-
const user = await users.findByEmail(
|
|
1296
|
+
const user = await users.findByEmail(email2);
|
|
1229
1297
|
if (!user) {
|
|
1230
1298
|
return setApiResponse3(
|
|
1231
1299
|
HTTP3.OK,
|
|
@@ -1251,7 +1319,7 @@ function authController(store, config, bus) {
|
|
|
1251
1319
|
await passwordReset.create(user.id, pin, expiresAt);
|
|
1252
1320
|
bus?.emit(NOTIFICATION_EVENT2, {
|
|
1253
1321
|
type: MESSAGE_KEYS.passwordReset,
|
|
1254
|
-
recipient: { email, phone: null, deviceToken: null },
|
|
1322
|
+
recipient: { email: email2, phone: null, deviceToken: null },
|
|
1255
1323
|
data: { pin }
|
|
1256
1324
|
}).catch(() => {
|
|
1257
1325
|
});
|
|
@@ -1264,8 +1332,8 @@ function authController(store, config, bus) {
|
|
|
1264
1332
|
resetPassword: async (ctx) => {
|
|
1265
1333
|
const body = ctx.meta["body"];
|
|
1266
1334
|
const raw = body?.["pin"];
|
|
1267
|
-
const
|
|
1268
|
-
if (typeof raw !== "string" || typeof
|
|
1335
|
+
const password2 = body?.["password"];
|
|
1336
|
+
if (typeof raw !== "string" || typeof password2 !== "string") {
|
|
1269
1337
|
return setApiResponse3(
|
|
1270
1338
|
HTTP3.UNPROCESSABLE,
|
|
1271
1339
|
"INVALID_PARAMETER",
|
|
@@ -1279,7 +1347,7 @@ function authController(store, config, bus) {
|
|
|
1279
1347
|
"pin must be a 6-digit code"
|
|
1280
1348
|
);
|
|
1281
1349
|
}
|
|
1282
|
-
if (
|
|
1350
|
+
if (password2.length < 8) {
|
|
1283
1351
|
return setApiResponse3(
|
|
1284
1352
|
HTTP3.UNPROCESSABLE,
|
|
1285
1353
|
"INVALID_PARAMETER",
|
|
@@ -1291,7 +1359,7 @@ function authController(store, config, bus) {
|
|
|
1291
1359
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1292
1360
|
return setApiResponse3(HTTP3.BAD_REQUEST, "PASSWORD_RESET_FAILED", "Invalid or expired pin");
|
|
1293
1361
|
}
|
|
1294
|
-
const passwordHash = await hashPassword(
|
|
1362
|
+
const passwordHash = await hashPassword(password2);
|
|
1295
1363
|
await store.transaction(async (tx) => {
|
|
1296
1364
|
await Promise.all([
|
|
1297
1365
|
tx.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
|
|
@@ -1398,8 +1466,8 @@ function authController(store, config, bus) {
|
|
|
1398
1466
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
1399
1467
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1400
1468
|
if (ctx.user.loginMethod === "phone") {
|
|
1401
|
-
const
|
|
1402
|
-
if (!
|
|
1469
|
+
const phone2 = ctx.user.phone;
|
|
1470
|
+
if (!phone2) {
|
|
1403
1471
|
return setApiResponse3(
|
|
1404
1472
|
HTTP3.BAD_REQUEST,
|
|
1405
1473
|
"NO_PHONE_ON_ACCOUNT",
|
|
@@ -1419,11 +1487,11 @@ function authController(store, config, bus) {
|
|
|
1419
1487
|
}
|
|
1420
1488
|
const otp = randomInt(1e5, 1e6).toString();
|
|
1421
1489
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
1422
|
-
await phoneVerif.upsert(ctx.user.id,
|
|
1490
|
+
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
1423
1491
|
bus?.emit(NOTIFICATION_EVENT2, {
|
|
1424
1492
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1425
1493
|
data: { otp },
|
|
1426
|
-
recipient: { email: null, phone, deviceToken: null }
|
|
1494
|
+
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
1427
1495
|
}).catch(() => {
|
|
1428
1496
|
});
|
|
1429
1497
|
return setApiResponse3(
|
|
@@ -1476,11 +1544,11 @@ function authController(store, config, bus) {
|
|
|
1476
1544
|
import { randomInt as randomInt2 } from "crypto";
|
|
1477
1545
|
import { setApiResponse as setApiResponse4, HTTP as HTTP4 } from "@fonderie/core";
|
|
1478
1546
|
import { NOTIFICATION_EVENT as NOTIFICATION_EVENT3 } from "@fonderie/events";
|
|
1479
|
-
function normalizePhone2(
|
|
1480
|
-
return
|
|
1547
|
+
function normalizePhone2(phone2) {
|
|
1548
|
+
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
1481
1549
|
}
|
|
1482
|
-
function isValidPhone2(
|
|
1483
|
-
return typeof
|
|
1550
|
+
function isValidPhone2(phone2) {
|
|
1551
|
+
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone2));
|
|
1484
1552
|
}
|
|
1485
1553
|
function userController(store, bus) {
|
|
1486
1554
|
const users = new UserModel(store);
|
|
@@ -1798,39 +1866,40 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1798
1866
|
const verifyGate = config.requireVerification ? requireVerified : (_ctx, next) => next();
|
|
1799
1867
|
const routes = [
|
|
1800
1868
|
// Registration & Login (Public)
|
|
1801
|
-
["POST", "/auth/register", auth.register],
|
|
1802
|
-
["POST", "/auth/login", auth.login],
|
|
1869
|
+
["POST", "/auth/register", validate(registerSchema), auth.register],
|
|
1870
|
+
["POST", "/auth/login", validate(loginSchema), auth.login],
|
|
1803
1871
|
// Token Management (Public)
|
|
1804
|
-
["POST", "/auth/refresh", auth.refresh],
|
|
1872
|
+
["POST", "/auth/refresh", validate(refreshSchema), auth.refresh],
|
|
1805
1873
|
// Email — Password Recovery (Public)
|
|
1806
|
-
["POST", "/auth/email/forgot", auth.forgotPassword],
|
|
1807
|
-
["POST", "/auth/email/reset", auth.resetPassword],
|
|
1874
|
+
["POST", "/auth/email/forgot", validate(forgotPasswordSchema), auth.forgotPassword],
|
|
1875
|
+
["POST", "/auth/email/reset", validate(resetPasswordSchema), auth.resetPassword],
|
|
1808
1876
|
// Verification (Protected — email or phone, determined by loginMethod)
|
|
1809
|
-
["POST", "/auth/verify", requireAuth, auth.verify],
|
|
1877
|
+
["POST", "/auth/verify", requireAuth, validate(verifySchema), auth.verify],
|
|
1810
1878
|
["GET", "/auth/send-verification", requireAuth, auth.sendVerification],
|
|
1811
1879
|
// Account Management (Protected)
|
|
1812
|
-
["POST", "/auth/logout", requireAuth, auth.logout],
|
|
1880
|
+
["POST", "/auth/logout", requireAuth, validate(refreshSchema), auth.logout],
|
|
1813
1881
|
// User Profile (Protected; writes also gate on requireVerification)
|
|
1814
1882
|
["GET", "/users", requireAuth, user.me],
|
|
1815
|
-
["PUT", "/users/profile", requireAuth, verifyGate, user.updateProfile],
|
|
1816
|
-
["PUT", "/users/preferences", requireAuth, verifyGate, user.updatePreferences],
|
|
1817
|
-
["PUT", "/users/email", requireAuth, verifyGate, user.updateEmail],
|
|
1818
|
-
["PUT", "/users/phone", requireAuth, verifyGate, user.updatePhone],
|
|
1819
|
-
["PUT", "/users/password", requireAuth, user.changePassword],
|
|
1883
|
+
["PUT", "/users/profile", requireAuth, verifyGate, validate(updateProfileSchema), user.updateProfile],
|
|
1884
|
+
["PUT", "/users/preferences", requireAuth, verifyGate, validate(updatePreferencesSchema), user.updatePreferences],
|
|
1885
|
+
["PUT", "/users/email", requireAuth, verifyGate, validate(updateEmailSchema), user.updateEmail],
|
|
1886
|
+
["PUT", "/users/phone", requireAuth, verifyGate, validate(updatePhoneSchema), user.updatePhone],
|
|
1887
|
+
["PUT", "/users/password", requireAuth, validate(changePasswordSchema), user.changePassword],
|
|
1820
1888
|
["DELETE", "/users", requireAuth, verifyGate, user.deleteMe],
|
|
1821
1889
|
// MFA (email sessions only — requireVerified is always enforced here
|
|
1822
1890
|
// because MFA is a security feature and email verification is meaningful)
|
|
1823
1891
|
["POST", "/auth/mfa/setup", requireAuth, requireEmailLogin, requireVerified, mfa.setup],
|
|
1824
1892
|
// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
|
|
1825
1893
|
// and full tokens (setup confirmation), so requireAnyAuth is used here.
|
|
1826
|
-
["POST", "/auth/mfa/verify", requireAnyAuth, requireEmailLogin, requireVerified, mfa.verify],
|
|
1827
|
-
["POST", "/auth/mfa/disable", requireAuth, requireEmailLogin, requireVerified, mfa.disable],
|
|
1894
|
+
["POST", "/auth/mfa/verify", requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify],
|
|
1895
|
+
["POST", "/auth/mfa/disable", requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.disable],
|
|
1828
1896
|
[
|
|
1829
1897
|
"POST",
|
|
1830
1898
|
"/auth/mfa/backup-codes",
|
|
1831
1899
|
requireAuth,
|
|
1832
1900
|
requireEmailLogin,
|
|
1833
1901
|
requireVerified,
|
|
1902
|
+
validate(mfaTokenSchema),
|
|
1834
1903
|
mfa.regenerateBackupCodes
|
|
1835
1904
|
]
|
|
1836
1905
|
];
|
|
@@ -1909,7 +1978,9 @@ export {
|
|
|
1909
1978
|
normalizeEmail,
|
|
1910
1979
|
normalizeEmailSafe,
|
|
1911
1980
|
requireAuth2 as requireAuth,
|
|
1981
|
+
schemas_exports as schemas,
|
|
1912
1982
|
toUserDTO,
|
|
1983
|
+
validate,
|
|
1913
1984
|
withSession
|
|
1914
1985
|
};
|
|
1915
1986
|
//# sourceMappingURL=index.js.map
|