@fonderie/auth 1.0.0 → 1.1.1
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 +195 -135
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -4
- package/dist/index.d.ts +83 -4
- package/dist/index.js +183 -125
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/migrations/index.d.ts +3 -0
- package/dist/{session-CLD1WPJs.d.cts → session-BGjjwz5_.d.cts} +1 -0
- package/dist/{session-CLD1WPJs.d.ts → session-BGjjwz5_.d.ts} +1 -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,94 @@ 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
|
+
|
|
116
|
+
// src/services/cookies.ts
|
|
117
|
+
function secureAttr(config) {
|
|
118
|
+
const secure = config.secureCookies ?? process.env["NODE_ENV"] === "production";
|
|
119
|
+
return secure ? "; Secure" : "";
|
|
120
|
+
}
|
|
121
|
+
function tokenPairCookies(accessToken, refreshToken, config) {
|
|
122
|
+
const secure = secureAttr(config);
|
|
123
|
+
return [
|
|
124
|
+
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/${secure}`,
|
|
125
|
+
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh${secure}`
|
|
126
|
+
].join(", ");
|
|
127
|
+
}
|
|
128
|
+
function clearedTokenCookies(config) {
|
|
129
|
+
const secure = secureAttr(config);
|
|
130
|
+
return [
|
|
131
|
+
`access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`,
|
|
132
|
+
`refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0${secure}`
|
|
133
|
+
].join(", ");
|
|
134
|
+
}
|
|
135
|
+
|
|
48
136
|
// src/controllers/mfa.controller.ts
|
|
49
137
|
import QRCode from "qrcode";
|
|
50
138
|
import { setApiResponse as setApiResponse2, HTTP as HTTP2 } from "@fonderie/core";
|
|
@@ -181,7 +269,7 @@ function base32Encode(buf) {
|
|
|
181
269
|
function generateTotpSecret() {
|
|
182
270
|
return base32Encode(randomBytes(20));
|
|
183
271
|
}
|
|
184
|
-
function generateTotpUri(
|
|
272
|
+
function generateTotpUri(email2, secret, issuer) {
|
|
185
273
|
const params = new URLSearchParams({
|
|
186
274
|
secret,
|
|
187
275
|
issuer,
|
|
@@ -189,7 +277,7 @@ function generateTotpUri(email, secret, issuer) {
|
|
|
189
277
|
digits: String(DIGITS),
|
|
190
278
|
period: String(STEP)
|
|
191
279
|
});
|
|
192
|
-
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(
|
|
280
|
+
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email2)}?${params}`;
|
|
193
281
|
}
|
|
194
282
|
function verifyTotpToken(token, secret) {
|
|
195
283
|
const t = timeCounter();
|
|
@@ -281,21 +369,21 @@ var UserModel = class {
|
|
|
281
369
|
);
|
|
282
370
|
return row ?? null;
|
|
283
371
|
}
|
|
284
|
-
async findByEmail(
|
|
372
|
+
async findByEmail(email2) {
|
|
285
373
|
const [row] = await this.store.query(
|
|
286
374
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,
|
|
287
|
-
[
|
|
375
|
+
[email2]
|
|
288
376
|
);
|
|
289
377
|
return row ?? null;
|
|
290
378
|
}
|
|
291
|
-
async findByPhone(
|
|
379
|
+
async findByPhone(phone2) {
|
|
292
380
|
const [row] = await this.store.query(
|
|
293
381
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
|
|
294
|
-
[
|
|
382
|
+
[phone2]
|
|
295
383
|
);
|
|
296
384
|
return row ?? null;
|
|
297
385
|
}
|
|
298
|
-
async findOrCreateByPhone(
|
|
386
|
+
async findOrCreateByPhone(phone2, firstName = null, lastName = null) {
|
|
299
387
|
const [row] = await this.store.query(
|
|
300
388
|
`INSERT INTO fonderie_users (phone, first_name, last_name)
|
|
301
389
|
VALUES ($1, $2, $3)
|
|
@@ -304,16 +392,16 @@ var UserModel = class {
|
|
|
304
392
|
last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),
|
|
305
393
|
updated_at = now()
|
|
306
394
|
RETURNING id`,
|
|
307
|
-
[
|
|
395
|
+
[phone2, firstName, lastName]
|
|
308
396
|
);
|
|
309
397
|
return row;
|
|
310
398
|
}
|
|
311
|
-
async create(
|
|
399
|
+
async create(email2, passwordHash, firstName, lastName) {
|
|
312
400
|
const [row] = await this.store.query(
|
|
313
401
|
`INSERT INTO fonderie_users (email, password_hash, first_name, last_name)
|
|
314
402
|
VALUES ($1, $2, $3, $4)
|
|
315
403
|
RETURNING id`,
|
|
316
|
-
[
|
|
404
|
+
[email2.toLowerCase().trim(), passwordHash, firstName, lastName]
|
|
317
405
|
);
|
|
318
406
|
return row ?? null;
|
|
319
407
|
}
|
|
@@ -400,16 +488,16 @@ var UserModel = class {
|
|
|
400
488
|
[id]
|
|
401
489
|
);
|
|
402
490
|
}
|
|
403
|
-
async updateEmail(id,
|
|
491
|
+
async updateEmail(id, email2) {
|
|
404
492
|
await this.store.query(
|
|
405
493
|
`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,
|
|
406
|
-
[
|
|
494
|
+
[email2.toLowerCase().trim(), id]
|
|
407
495
|
);
|
|
408
496
|
}
|
|
409
|
-
async updatePhone(id,
|
|
497
|
+
async updatePhone(id, phone2) {
|
|
410
498
|
await this.store.query(
|
|
411
499
|
`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,
|
|
412
|
-
[
|
|
500
|
+
[phone2, id]
|
|
413
501
|
);
|
|
414
502
|
}
|
|
415
503
|
async updatePreferences(id, fields) {
|
|
@@ -442,14 +530,14 @@ var UserModel = class {
|
|
|
442
530
|
);
|
|
443
531
|
return row?.mfa_secret ?? null;
|
|
444
532
|
}
|
|
445
|
-
async upsertByProvider(
|
|
533
|
+
async upsertByProvider(email2, provider, providerId) {
|
|
446
534
|
const [row] = await this.store.query(
|
|
447
535
|
`INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
|
|
448
536
|
VALUES ($1, now(), $2, $3)
|
|
449
537
|
ON CONFLICT (email) DO UPDATE
|
|
450
538
|
SET provider = $2, provider_id = $3
|
|
451
539
|
RETURNING id`,
|
|
452
|
-
[
|
|
540
|
+
[email2, provider, providerId]
|
|
453
541
|
);
|
|
454
542
|
return row ?? null;
|
|
455
543
|
}
|
|
@@ -629,10 +717,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
629
717
|
{
|
|
630
718
|
status: 200,
|
|
631
719
|
headers: {
|
|
632
|
-
"Set-Cookie":
|
|
633
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
634
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
635
|
-
].join(", ")
|
|
720
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
636
721
|
}
|
|
637
722
|
}
|
|
638
723
|
);
|
|
@@ -706,11 +791,11 @@ function checkCooldown(lastSentAt, cooldownMs) {
|
|
|
706
791
|
init_password();
|
|
707
792
|
|
|
708
793
|
// src/services/email.ts
|
|
709
|
-
function normalizeEmail(
|
|
710
|
-
if (typeof
|
|
794
|
+
function normalizeEmail(email2) {
|
|
795
|
+
if (typeof email2 !== "string" || email2.length === 0) {
|
|
711
796
|
throw new Error("Invalid email");
|
|
712
797
|
}
|
|
713
|
-
const lower =
|
|
798
|
+
const lower = email2.trim().toLowerCase();
|
|
714
799
|
if (lower.length === 0) {
|
|
715
800
|
throw new Error("Email cannot be empty");
|
|
716
801
|
}
|
|
@@ -730,9 +815,9 @@ function normalizeEmail(email) {
|
|
|
730
815
|
}
|
|
731
816
|
return `${normalizedLocal}@${domain}`;
|
|
732
817
|
}
|
|
733
|
-
function normalizeEmailSafe(
|
|
818
|
+
function normalizeEmailSafe(email2) {
|
|
734
819
|
try {
|
|
735
|
-
return normalizeEmail(
|
|
820
|
+
return normalizeEmail(email2);
|
|
736
821
|
} catch {
|
|
737
822
|
return null;
|
|
738
823
|
}
|
|
@@ -833,13 +918,13 @@ var PhoneVerificationModel = class {
|
|
|
833
918
|
this.store = store;
|
|
834
919
|
}
|
|
835
920
|
store;
|
|
836
|
-
async upsert(userId,
|
|
921
|
+
async upsert(userId, phone2, otp, expiresAt) {
|
|
837
922
|
await this.store.query(
|
|
838
923
|
`INSERT INTO fonderie_phone_verifications (phone, user_id, otp, expires_at)
|
|
839
924
|
VALUES ($1, $2, $3, $4)
|
|
840
925
|
ON CONFLICT (phone) DO UPDATE
|
|
841
926
|
SET user_id = $2, otp = $3, expires_at = $4, created_at = now()`,
|
|
842
|
-
[
|
|
927
|
+
[phone2, userId, otp, expiresAt]
|
|
843
928
|
);
|
|
844
929
|
}
|
|
845
930
|
async findByUser(userId, otp) {
|
|
@@ -864,11 +949,11 @@ var PhoneVerificationModel = class {
|
|
|
864
949
|
};
|
|
865
950
|
|
|
866
951
|
// src/controllers/auth.controller.ts
|
|
867
|
-
function normalizePhone(
|
|
868
|
-
return
|
|
952
|
+
function normalizePhone(phone2) {
|
|
953
|
+
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
869
954
|
}
|
|
870
|
-
function isValidPhone(
|
|
871
|
-
return typeof
|
|
955
|
+
function isValidPhone(phone2) {
|
|
956
|
+
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone(phone2));
|
|
872
957
|
}
|
|
873
958
|
function extractRefreshToken(ctx) {
|
|
874
959
|
const body = ctx.meta["body"];
|
|
@@ -889,13 +974,13 @@ function authController(store, config, bus) {
|
|
|
889
974
|
return {
|
|
890
975
|
register: async (ctx) => {
|
|
891
976
|
const body = ctx.meta["body"];
|
|
892
|
-
const { email, password, phone, firstName = null, lastName = null } = body ?? {};
|
|
893
|
-
if (typeof
|
|
894
|
-
const normalizedEmail = normalizeEmailSafe(
|
|
977
|
+
const { email: email2, password: password2, phone: phone2, firstName = null, lastName = null } = body ?? {};
|
|
978
|
+
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
979
|
+
const normalizedEmail = normalizeEmailSafe(email2);
|
|
895
980
|
if (!normalizedEmail) {
|
|
896
981
|
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
897
982
|
}
|
|
898
|
-
if (
|
|
983
|
+
if (password2.length < 8) {
|
|
899
984
|
return setApiResponse3(
|
|
900
985
|
HTTP3.UNPROCESSABLE,
|
|
901
986
|
"INVALID_PARAMETER",
|
|
@@ -906,7 +991,7 @@ function authController(store, config, bus) {
|
|
|
906
991
|
if (existing) {
|
|
907
992
|
return setApiResponse3(HTTP3.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
908
993
|
}
|
|
909
|
-
const passwordHash = await hashPassword(
|
|
994
|
+
const passwordHash = await hashPassword(password2);
|
|
910
995
|
const row = await users.create(
|
|
911
996
|
normalizedEmail,
|
|
912
997
|
passwordHash,
|
|
@@ -966,27 +1051,24 @@ function authController(store, config, bus) {
|
|
|
966
1051
|
{
|
|
967
1052
|
status: 201,
|
|
968
1053
|
headers: {
|
|
969
|
-
"Set-Cookie":
|
|
970
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
971
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
972
|
-
].join(", ")
|
|
1054
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
973
1055
|
}
|
|
974
1056
|
}
|
|
975
1057
|
);
|
|
976
1058
|
}
|
|
977
|
-
if (isValidPhone(
|
|
978
|
-
const existing = await users.findByPhone(normalizePhone(
|
|
1059
|
+
if (isValidPhone(phone2)) {
|
|
1060
|
+
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
979
1061
|
if (existing) {
|
|
980
1062
|
return setApiResponse3(HTTP3.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
981
1063
|
}
|
|
982
1064
|
const { id } = await users.findOrCreateByPhone(
|
|
983
|
-
normalizePhone(
|
|
1065
|
+
normalizePhone(phone2),
|
|
984
1066
|
firstName ?? null,
|
|
985
1067
|
lastName ?? null
|
|
986
1068
|
);
|
|
987
1069
|
const otp = randomInt(1e5, 1e6).toString();
|
|
988
1070
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
989
|
-
await phoneVerif.upsert(id, normalizePhone(
|
|
1071
|
+
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
990
1072
|
const user = await users.findById(id);
|
|
991
1073
|
if (!user) {
|
|
992
1074
|
return setApiResponse3(HTTP3.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
@@ -998,7 +1080,7 @@ function authController(store, config, bus) {
|
|
|
998
1080
|
{
|
|
999
1081
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1000
1082
|
data: { otp },
|
|
1001
|
-
recipient: { email: null, phone: normalizePhone(
|
|
1083
|
+
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1002
1084
|
},
|
|
1003
1085
|
reqOpts2
|
|
1004
1086
|
).catch(() => {
|
|
@@ -1031,10 +1113,7 @@ function authController(store, config, bus) {
|
|
|
1031
1113
|
{
|
|
1032
1114
|
status: 202,
|
|
1033
1115
|
headers: {
|
|
1034
|
-
"Set-Cookie":
|
|
1035
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1036
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1037
|
-
].join(", ")
|
|
1116
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1038
1117
|
}
|
|
1039
1118
|
}
|
|
1040
1119
|
);
|
|
@@ -1048,16 +1127,16 @@ function authController(store, config, bus) {
|
|
|
1048
1127
|
login: async (ctx) => {
|
|
1049
1128
|
const body = ctx.meta["body"];
|
|
1050
1129
|
if (typeof body?.["email"] === "string" && typeof body?.["password"] === "string") {
|
|
1051
|
-
const { email: rawEmail, password } = body;
|
|
1052
|
-
const
|
|
1053
|
-
if (!
|
|
1130
|
+
const { email: rawEmail, password: password2 } = body;
|
|
1131
|
+
const email2 = normalizeEmailSafe(rawEmail);
|
|
1132
|
+
if (!email2) {
|
|
1054
1133
|
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1055
1134
|
}
|
|
1056
|
-
const user = await users.findByEmail(
|
|
1135
|
+
const user = await users.findByEmail(email2);
|
|
1057
1136
|
if (!user || !user.passwordHash) {
|
|
1058
1137
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1059
1138
|
}
|
|
1060
|
-
const valid = await verifyPassword(
|
|
1139
|
+
const valid = await verifyPassword(password2, user.passwordHash);
|
|
1061
1140
|
if (!valid) {
|
|
1062
1141
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1063
1142
|
}
|
|
@@ -1093,17 +1172,14 @@ function authController(store, config, bus) {
|
|
|
1093
1172
|
{
|
|
1094
1173
|
status: 200,
|
|
1095
1174
|
headers: {
|
|
1096
|
-
"Set-Cookie":
|
|
1097
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1098
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1099
|
-
].join(", ")
|
|
1175
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1100
1176
|
}
|
|
1101
1177
|
}
|
|
1102
1178
|
);
|
|
1103
1179
|
}
|
|
1104
|
-
const
|
|
1105
|
-
if (isValidPhone(
|
|
1106
|
-
const user = await users.findByPhone(normalizePhone(
|
|
1180
|
+
const phone2 = body?.["phone"];
|
|
1181
|
+
if (isValidPhone(phone2)) {
|
|
1182
|
+
const user = await users.findByPhone(normalizePhone(phone2));
|
|
1107
1183
|
if (!user) {
|
|
1108
1184
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1109
1185
|
}
|
|
@@ -1116,11 +1192,11 @@ function authController(store, config, bus) {
|
|
|
1116
1192
|
}
|
|
1117
1193
|
const otp = randomInt(1e5, 1e6).toString();
|
|
1118
1194
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1119
|
-
await phoneVerif.upsert(user.id, normalizePhone(
|
|
1195
|
+
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1120
1196
|
bus?.emit(NOTIFICATION_EVENT2, {
|
|
1121
1197
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1122
1198
|
data: { otp },
|
|
1123
|
-
recipient: { email: null, phone: normalizePhone(
|
|
1199
|
+
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1124
1200
|
}).catch(() => {
|
|
1125
1201
|
});
|
|
1126
1202
|
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
@@ -1139,10 +1215,7 @@ function authController(store, config, bus) {
|
|
|
1139
1215
|
{
|
|
1140
1216
|
status: 202,
|
|
1141
1217
|
headers: {
|
|
1142
|
-
"Set-Cookie":
|
|
1143
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1144
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1145
|
-
].join(", ")
|
|
1218
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1146
1219
|
}
|
|
1147
1220
|
}
|
|
1148
1221
|
);
|
|
@@ -1163,10 +1236,7 @@ function authController(store, config, bus) {
|
|
|
1163
1236
|
{
|
|
1164
1237
|
status: 200,
|
|
1165
1238
|
headers: {
|
|
1166
|
-
"Set-Cookie":
|
|
1167
|
-
"access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
|
|
1168
|
-
"refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
|
|
1169
|
-
].join(", ")
|
|
1239
|
+
"Set-Cookie": clearedTokenCookies(config)
|
|
1170
1240
|
}
|
|
1171
1241
|
}
|
|
1172
1242
|
);
|
|
@@ -1207,10 +1277,7 @@ function authController(store, config, bus) {
|
|
|
1207
1277
|
{
|
|
1208
1278
|
status: 200,
|
|
1209
1279
|
headers: {
|
|
1210
|
-
"Set-Cookie":
|
|
1211
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1212
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1213
|
-
].join(", ")
|
|
1280
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1214
1281
|
}
|
|
1215
1282
|
}
|
|
1216
1283
|
);
|
|
@@ -1221,11 +1288,11 @@ function authController(store, config, bus) {
|
|
|
1221
1288
|
if (typeof rawEmail !== "string") {
|
|
1222
1289
|
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
1223
1290
|
}
|
|
1224
|
-
const
|
|
1225
|
-
if (!
|
|
1291
|
+
const email2 = normalizeEmailSafe(rawEmail);
|
|
1292
|
+
if (!email2) {
|
|
1226
1293
|
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1227
1294
|
}
|
|
1228
|
-
const user = await users.findByEmail(
|
|
1295
|
+
const user = await users.findByEmail(email2);
|
|
1229
1296
|
if (!user) {
|
|
1230
1297
|
return setApiResponse3(
|
|
1231
1298
|
HTTP3.OK,
|
|
@@ -1238,12 +1305,9 @@ function authController(store, config, bus) {
|
|
|
1238
1305
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
1239
1306
|
if (remaining > 0) {
|
|
1240
1307
|
return setApiResponse3(
|
|
1241
|
-
HTTP3.
|
|
1242
|
-
"
|
|
1243
|
-
"
|
|
1244
|
-
{
|
|
1245
|
-
retryAfter: Math.ceil(remaining / 1e3)
|
|
1246
|
-
}
|
|
1308
|
+
HTTP3.OK,
|
|
1309
|
+
"PASSWORD_RESET_EMAIL_SENT",
|
|
1310
|
+
"Password reset email sent (if account exists)."
|
|
1247
1311
|
);
|
|
1248
1312
|
}
|
|
1249
1313
|
const pin = randomInt(1e5, 1e6).toString();
|
|
@@ -1251,7 +1315,7 @@ function authController(store, config, bus) {
|
|
|
1251
1315
|
await passwordReset.create(user.id, pin, expiresAt);
|
|
1252
1316
|
bus?.emit(NOTIFICATION_EVENT2, {
|
|
1253
1317
|
type: MESSAGE_KEYS.passwordReset,
|
|
1254
|
-
recipient: { email, phone: null, deviceToken: null },
|
|
1318
|
+
recipient: { email: email2, phone: null, deviceToken: null },
|
|
1255
1319
|
data: { pin }
|
|
1256
1320
|
}).catch(() => {
|
|
1257
1321
|
});
|
|
@@ -1264,8 +1328,8 @@ function authController(store, config, bus) {
|
|
|
1264
1328
|
resetPassword: async (ctx) => {
|
|
1265
1329
|
const body = ctx.meta["body"];
|
|
1266
1330
|
const raw = body?.["pin"];
|
|
1267
|
-
const
|
|
1268
|
-
if (typeof raw !== "string" || typeof
|
|
1331
|
+
const password2 = body?.["password"];
|
|
1332
|
+
if (typeof raw !== "string" || typeof password2 !== "string") {
|
|
1269
1333
|
return setApiResponse3(
|
|
1270
1334
|
HTTP3.UNPROCESSABLE,
|
|
1271
1335
|
"INVALID_PARAMETER",
|
|
@@ -1279,7 +1343,7 @@ function authController(store, config, bus) {
|
|
|
1279
1343
|
"pin must be a 6-digit code"
|
|
1280
1344
|
);
|
|
1281
1345
|
}
|
|
1282
|
-
if (
|
|
1346
|
+
if (password2.length < 8) {
|
|
1283
1347
|
return setApiResponse3(
|
|
1284
1348
|
HTTP3.UNPROCESSABLE,
|
|
1285
1349
|
"INVALID_PARAMETER",
|
|
@@ -1291,7 +1355,7 @@ function authController(store, config, bus) {
|
|
|
1291
1355
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1292
1356
|
return setApiResponse3(HTTP3.BAD_REQUEST, "PASSWORD_RESET_FAILED", "Invalid or expired pin");
|
|
1293
1357
|
}
|
|
1294
|
-
const passwordHash = await hashPassword(
|
|
1358
|
+
const passwordHash = await hashPassword(password2);
|
|
1295
1359
|
await store.transaction(async (tx) => {
|
|
1296
1360
|
await Promise.all([
|
|
1297
1361
|
tx.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
|
|
@@ -1362,10 +1426,7 @@ function authController(store, config, bus) {
|
|
|
1362
1426
|
{
|
|
1363
1427
|
status: 200,
|
|
1364
1428
|
headers: {
|
|
1365
|
-
"Set-Cookie":
|
|
1366
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1367
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1368
|
-
].join(", ")
|
|
1429
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1369
1430
|
}
|
|
1370
1431
|
}
|
|
1371
1432
|
);
|
|
@@ -1398,8 +1459,8 @@ function authController(store, config, bus) {
|
|
|
1398
1459
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
1399
1460
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1400
1461
|
if (ctx.user.loginMethod === "phone") {
|
|
1401
|
-
const
|
|
1402
|
-
if (!
|
|
1462
|
+
const phone2 = ctx.user.phone;
|
|
1463
|
+
if (!phone2) {
|
|
1403
1464
|
return setApiResponse3(
|
|
1404
1465
|
HTTP3.BAD_REQUEST,
|
|
1405
1466
|
"NO_PHONE_ON_ACCOUNT",
|
|
@@ -1419,11 +1480,11 @@ function authController(store, config, bus) {
|
|
|
1419
1480
|
}
|
|
1420
1481
|
const otp = randomInt(1e5, 1e6).toString();
|
|
1421
1482
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
1422
|
-
await phoneVerif.upsert(ctx.user.id,
|
|
1483
|
+
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
1423
1484
|
bus?.emit(NOTIFICATION_EVENT2, {
|
|
1424
1485
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1425
1486
|
data: { otp },
|
|
1426
|
-
recipient: { email: null, phone, deviceToken: null }
|
|
1487
|
+
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
1427
1488
|
}).catch(() => {
|
|
1428
1489
|
});
|
|
1429
1490
|
return setApiResponse3(
|
|
@@ -1476,13 +1537,13 @@ function authController(store, config, bus) {
|
|
|
1476
1537
|
import { randomInt as randomInt2 } from "crypto";
|
|
1477
1538
|
import { setApiResponse as setApiResponse4, HTTP as HTTP4 } from "@fonderie/core";
|
|
1478
1539
|
import { NOTIFICATION_EVENT as NOTIFICATION_EVENT3 } from "@fonderie/events";
|
|
1479
|
-
function normalizePhone2(
|
|
1480
|
-
return
|
|
1540
|
+
function normalizePhone2(phone2) {
|
|
1541
|
+
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
1481
1542
|
}
|
|
1482
|
-
function isValidPhone2(
|
|
1483
|
-
return typeof
|
|
1543
|
+
function isValidPhone2(phone2) {
|
|
1544
|
+
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone2));
|
|
1484
1545
|
}
|
|
1485
|
-
function userController(store, bus) {
|
|
1546
|
+
function userController(store, config, bus) {
|
|
1486
1547
|
const users = new UserModel(store);
|
|
1487
1548
|
const emailVerif = new EmailVerificationModel(store);
|
|
1488
1549
|
const phoneVerif = new PhoneVerificationModel(store);
|
|
@@ -1671,10 +1732,7 @@ function userController(store, bus) {
|
|
|
1671
1732
|
{
|
|
1672
1733
|
status: 200,
|
|
1673
1734
|
headers: {
|
|
1674
|
-
"Set-Cookie":
|
|
1675
|
-
"access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
|
|
1676
|
-
"refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
|
|
1677
|
-
].join(", ")
|
|
1735
|
+
"Set-Cookie": clearedTokenCookies(config)
|
|
1678
1736
|
}
|
|
1679
1737
|
}
|
|
1680
1738
|
);
|
|
@@ -1778,10 +1836,7 @@ function oauthController(store, config) {
|
|
|
1778
1836
|
{
|
|
1779
1837
|
status: 200,
|
|
1780
1838
|
headers: {
|
|
1781
|
-
"Set-Cookie":
|
|
1782
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1783
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1784
|
-
].join(", ")
|
|
1839
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1785
1840
|
}
|
|
1786
1841
|
}
|
|
1787
1842
|
);
|
|
@@ -1791,46 +1846,47 @@ function oauthController(store, config) {
|
|
|
1791
1846
|
|
|
1792
1847
|
// src/routes.ts
|
|
1793
1848
|
function buildAuthRoutes(store, config, bus) {
|
|
1794
|
-
const user = userController(store, bus);
|
|
1849
|
+
const user = userController(store, config, bus);
|
|
1795
1850
|
const auth = authController(store, config, bus);
|
|
1796
1851
|
const oauth = oauthController(store, config);
|
|
1797
1852
|
const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
|
|
1798
1853
|
const verifyGate = config.requireVerification ? requireVerified : (_ctx, next) => next();
|
|
1799
1854
|
const routes = [
|
|
1800
1855
|
// Registration & Login (Public)
|
|
1801
|
-
["POST", "/auth/register", auth.register],
|
|
1802
|
-
["POST", "/auth/login", auth.login],
|
|
1856
|
+
["POST", "/auth/register", validate(registerSchema), auth.register],
|
|
1857
|
+
["POST", "/auth/login", validate(loginSchema), auth.login],
|
|
1803
1858
|
// Token Management (Public)
|
|
1804
|
-
["POST", "/auth/refresh", auth.refresh],
|
|
1859
|
+
["POST", "/auth/refresh", validate(refreshSchema), auth.refresh],
|
|
1805
1860
|
// Email — Password Recovery (Public)
|
|
1806
|
-
["POST", "/auth/email/forgot", auth.forgotPassword],
|
|
1807
|
-
["POST", "/auth/email/reset", auth.resetPassword],
|
|
1861
|
+
["POST", "/auth/email/forgot", validate(forgotPasswordSchema), auth.forgotPassword],
|
|
1862
|
+
["POST", "/auth/email/reset", validate(resetPasswordSchema), auth.resetPassword],
|
|
1808
1863
|
// Verification (Protected — email or phone, determined by loginMethod)
|
|
1809
|
-
["POST", "/auth/verify", requireAuth, auth.verify],
|
|
1864
|
+
["POST", "/auth/verify", requireAuth, validate(verifySchema), auth.verify],
|
|
1810
1865
|
["GET", "/auth/send-verification", requireAuth, auth.sendVerification],
|
|
1811
1866
|
// Account Management (Protected)
|
|
1812
|
-
["POST", "/auth/logout", requireAuth, auth.logout],
|
|
1867
|
+
["POST", "/auth/logout", requireAuth, validate(refreshSchema), auth.logout],
|
|
1813
1868
|
// User Profile (Protected; writes also gate on requireVerification)
|
|
1814
1869
|
["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],
|
|
1870
|
+
["PUT", "/users/profile", requireAuth, verifyGate, validate(updateProfileSchema), user.updateProfile],
|
|
1871
|
+
["PUT", "/users/preferences", requireAuth, verifyGate, validate(updatePreferencesSchema), user.updatePreferences],
|
|
1872
|
+
["PUT", "/users/email", requireAuth, verifyGate, validate(updateEmailSchema), user.updateEmail],
|
|
1873
|
+
["PUT", "/users/phone", requireAuth, verifyGate, validate(updatePhoneSchema), user.updatePhone],
|
|
1874
|
+
["PUT", "/users/password", requireAuth, validate(changePasswordSchema), user.changePassword],
|
|
1820
1875
|
["DELETE", "/users", requireAuth, verifyGate, user.deleteMe],
|
|
1821
1876
|
// MFA (email sessions only — requireVerified is always enforced here
|
|
1822
1877
|
// because MFA is a security feature and email verification is meaningful)
|
|
1823
1878
|
["POST", "/auth/mfa/setup", requireAuth, requireEmailLogin, requireVerified, mfa.setup],
|
|
1824
1879
|
// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
|
|
1825
1880
|
// 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],
|
|
1881
|
+
["POST", "/auth/mfa/verify", requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify],
|
|
1882
|
+
["POST", "/auth/mfa/disable", requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.disable],
|
|
1828
1883
|
[
|
|
1829
1884
|
"POST",
|
|
1830
1885
|
"/auth/mfa/backup-codes",
|
|
1831
1886
|
requireAuth,
|
|
1832
1887
|
requireEmailLogin,
|
|
1833
1888
|
requireVerified,
|
|
1889
|
+
validate(mfaTokenSchema),
|
|
1834
1890
|
mfa.regenerateBackupCodes
|
|
1835
1891
|
]
|
|
1836
1892
|
];
|
|
@@ -1909,7 +1965,9 @@ export {
|
|
|
1909
1965
|
normalizeEmail,
|
|
1910
1966
|
normalizeEmailSafe,
|
|
1911
1967
|
requireAuth2 as requireAuth,
|
|
1968
|
+
schemas_exports as schemas,
|
|
1912
1969
|
toUserDTO,
|
|
1970
|
+
validate,
|
|
1913
1971
|
withSession
|
|
1914
1972
|
};
|
|
1915
1973
|
//# sourceMappingURL=index.js.map
|