@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.cjs
CHANGED
|
@@ -59,14 +59,16 @@ __export(index_exports, {
|
|
|
59
59
|
MESSAGE_KEYS: () => MESSAGE_KEYS,
|
|
60
60
|
normalizeEmail: () => normalizeEmail,
|
|
61
61
|
normalizeEmailSafe: () => normalizeEmailSafe,
|
|
62
|
-
requireAuth: () =>
|
|
62
|
+
requireAuth: () => import_middlewares3.requireAuth,
|
|
63
|
+
schemas: () => schemas_exports,
|
|
63
64
|
toUserDTO: () => toUserDTO,
|
|
65
|
+
validate: () => import_middlewares.validate,
|
|
64
66
|
withSession: () => withSession
|
|
65
67
|
});
|
|
66
68
|
module.exports = __toCommonJS(index_exports);
|
|
67
69
|
|
|
68
70
|
// src/routes.ts
|
|
69
|
-
var
|
|
71
|
+
var import_middlewares2 = require("@fonderie/core/middlewares");
|
|
70
72
|
|
|
71
73
|
// src/middlewares/require-email-login.ts
|
|
72
74
|
var import_core = require("@fonderie/core");
|
|
@@ -81,6 +83,94 @@ var requireEmailLogin = async (ctx, next) => {
|
|
|
81
83
|
return next();
|
|
82
84
|
};
|
|
83
85
|
|
|
86
|
+
// src/middlewares/validate.ts
|
|
87
|
+
var import_middlewares = require("@fonderie/core/middlewares");
|
|
88
|
+
|
|
89
|
+
// src/schemas.ts
|
|
90
|
+
var schemas_exports = {};
|
|
91
|
+
__export(schemas_exports, {
|
|
92
|
+
changePasswordSchema: () => changePasswordSchema,
|
|
93
|
+
forgotPasswordSchema: () => forgotPasswordSchema,
|
|
94
|
+
loginSchema: () => loginSchema,
|
|
95
|
+
mfaTokenSchema: () => mfaTokenSchema,
|
|
96
|
+
refreshSchema: () => refreshSchema,
|
|
97
|
+
registerSchema: () => registerSchema,
|
|
98
|
+
resetPasswordSchema: () => resetPasswordSchema,
|
|
99
|
+
updateEmailSchema: () => updateEmailSchema,
|
|
100
|
+
updatePhoneSchema: () => updatePhoneSchema,
|
|
101
|
+
updatePreferencesSchema: () => updatePreferencesSchema,
|
|
102
|
+
updateProfileSchema: () => updateProfileSchema,
|
|
103
|
+
verifySchema: () => verifySchema
|
|
104
|
+
});
|
|
105
|
+
var import_zod = require("zod");
|
|
106
|
+
var email = import_zod.z.string().trim().pipe(import_zod.z.email());
|
|
107
|
+
var password = import_zod.z.string().min(8, "password must be at least 8 characters").max(128);
|
|
108
|
+
var phone = import_zod.z.string().refine((v) => /^\+?[1-9]\d{6,14}$/.test(v.replace(/[\s\-()]/g, "")), "Invalid phone number");
|
|
109
|
+
var sixDigitPin = import_zod.z.string().trim().regex(/^\d{6}$/, "must be a 6-digit code");
|
|
110
|
+
var registerSchema = import_zod.z.union([
|
|
111
|
+
import_zod.z.object({
|
|
112
|
+
email,
|
|
113
|
+
password,
|
|
114
|
+
firstName: import_zod.z.string().max(100).nullish(),
|
|
115
|
+
lastName: import_zod.z.string().max(100).nullish()
|
|
116
|
+
}),
|
|
117
|
+
import_zod.z.object({ phone })
|
|
118
|
+
]);
|
|
119
|
+
var loginSchema = import_zod.z.union([
|
|
120
|
+
import_zod.z.object({ email, password: import_zod.z.string().min(1).max(128) }),
|
|
121
|
+
import_zod.z.object({ phone })
|
|
122
|
+
]);
|
|
123
|
+
var refreshSchema = import_zod.z.object({ refreshToken: import_zod.z.string().min(1).optional() });
|
|
124
|
+
var forgotPasswordSchema = import_zod.z.object({ email });
|
|
125
|
+
var resetPasswordSchema = import_zod.z.object({ pin: sixDigitPin, password });
|
|
126
|
+
var verifySchema = import_zod.z.object({ token: sixDigitPin });
|
|
127
|
+
var updateProfileSchema = import_zod.z.object({
|
|
128
|
+
firstName: import_zod.z.string().max(100).nullable().optional(),
|
|
129
|
+
lastName: import_zod.z.string().max(100).nullable().optional(),
|
|
130
|
+
avatarUrl: import_zod.z.string().trim().pipe(import_zod.z.url()).nullable().optional()
|
|
131
|
+
}).refine(
|
|
132
|
+
(o) => Object.values(o).some((v) => v !== void 0),
|
|
133
|
+
"Provide at least one of: firstName, lastName, avatarUrl"
|
|
134
|
+
);
|
|
135
|
+
var updatePreferencesSchema = import_zod.z.object({
|
|
136
|
+
locale: import_zod.z.string().max(35).optional(),
|
|
137
|
+
timezone: import_zod.z.string().max(64).optional(),
|
|
138
|
+
notifications: import_zod.z.unknown().optional(),
|
|
139
|
+
emailDigest: import_zod.z.unknown().optional(),
|
|
140
|
+
dateFormat: import_zod.z.unknown().optional(),
|
|
141
|
+
timeFormat: import_zod.z.unknown().optional()
|
|
142
|
+
}).refine(
|
|
143
|
+
(o) => Object.values(o).some((v) => v !== void 0),
|
|
144
|
+
"Provide at least one preference field"
|
|
145
|
+
);
|
|
146
|
+
var updateEmailSchema = import_zod.z.object({ email });
|
|
147
|
+
var updatePhoneSchema = import_zod.z.object({ phone });
|
|
148
|
+
var changePasswordSchema = import_zod.z.object({
|
|
149
|
+
currentPassword: import_zod.z.string().min(1).max(128),
|
|
150
|
+
newPassword: password
|
|
151
|
+
});
|
|
152
|
+
var mfaTokenSchema = import_zod.z.object({ token: import_zod.z.string().trim().min(6).max(64) });
|
|
153
|
+
|
|
154
|
+
// src/services/cookies.ts
|
|
155
|
+
function secureAttr(config) {
|
|
156
|
+
const secure = config.secureCookies ?? process.env["NODE_ENV"] === "production";
|
|
157
|
+
return secure ? "; Secure" : "";
|
|
158
|
+
}
|
|
159
|
+
function tokenPairCookies(accessToken, refreshToken, config) {
|
|
160
|
+
const secure = secureAttr(config);
|
|
161
|
+
return [
|
|
162
|
+
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/${secure}`,
|
|
163
|
+
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh${secure}`
|
|
164
|
+
].join(", ");
|
|
165
|
+
}
|
|
166
|
+
function clearedTokenCookies(config) {
|
|
167
|
+
const secure = secureAttr(config);
|
|
168
|
+
return [
|
|
169
|
+
`access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`,
|
|
170
|
+
`refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0${secure}`
|
|
171
|
+
].join(", ");
|
|
172
|
+
}
|
|
173
|
+
|
|
84
174
|
// src/controllers/mfa.controller.ts
|
|
85
175
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
86
176
|
var import_core3 = require("@fonderie/core");
|
|
@@ -217,7 +307,7 @@ function base32Encode(buf) {
|
|
|
217
307
|
function generateTotpSecret() {
|
|
218
308
|
return base32Encode((0, import_node_crypto.randomBytes)(20));
|
|
219
309
|
}
|
|
220
|
-
function generateTotpUri(
|
|
310
|
+
function generateTotpUri(email2, secret, issuer) {
|
|
221
311
|
const params = new URLSearchParams({
|
|
222
312
|
secret,
|
|
223
313
|
issuer,
|
|
@@ -225,7 +315,7 @@ function generateTotpUri(email, secret, issuer) {
|
|
|
225
315
|
digits: String(DIGITS),
|
|
226
316
|
period: String(STEP)
|
|
227
317
|
});
|
|
228
|
-
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(
|
|
318
|
+
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email2)}?${params}`;
|
|
229
319
|
}
|
|
230
320
|
function verifyTotpToken(token, secret) {
|
|
231
321
|
const t = timeCounter();
|
|
@@ -317,21 +407,21 @@ var UserModel = class {
|
|
|
317
407
|
);
|
|
318
408
|
return row ?? null;
|
|
319
409
|
}
|
|
320
|
-
async findByEmail(
|
|
410
|
+
async findByEmail(email2) {
|
|
321
411
|
const [row] = await this.store.query(
|
|
322
412
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,
|
|
323
|
-
[
|
|
413
|
+
[email2]
|
|
324
414
|
);
|
|
325
415
|
return row ?? null;
|
|
326
416
|
}
|
|
327
|
-
async findByPhone(
|
|
417
|
+
async findByPhone(phone2) {
|
|
328
418
|
const [row] = await this.store.query(
|
|
329
419
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
|
|
330
|
-
[
|
|
420
|
+
[phone2]
|
|
331
421
|
);
|
|
332
422
|
return row ?? null;
|
|
333
423
|
}
|
|
334
|
-
async findOrCreateByPhone(
|
|
424
|
+
async findOrCreateByPhone(phone2, firstName = null, lastName = null) {
|
|
335
425
|
const [row] = await this.store.query(
|
|
336
426
|
`INSERT INTO fonderie_users (phone, first_name, last_name)
|
|
337
427
|
VALUES ($1, $2, $3)
|
|
@@ -340,16 +430,16 @@ var UserModel = class {
|
|
|
340
430
|
last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),
|
|
341
431
|
updated_at = now()
|
|
342
432
|
RETURNING id`,
|
|
343
|
-
[
|
|
433
|
+
[phone2, firstName, lastName]
|
|
344
434
|
);
|
|
345
435
|
return row;
|
|
346
436
|
}
|
|
347
|
-
async create(
|
|
437
|
+
async create(email2, passwordHash, firstName, lastName) {
|
|
348
438
|
const [row] = await this.store.query(
|
|
349
439
|
`INSERT INTO fonderie_users (email, password_hash, first_name, last_name)
|
|
350
440
|
VALUES ($1, $2, $3, $4)
|
|
351
441
|
RETURNING id`,
|
|
352
|
-
[
|
|
442
|
+
[email2.toLowerCase().trim(), passwordHash, firstName, lastName]
|
|
353
443
|
);
|
|
354
444
|
return row ?? null;
|
|
355
445
|
}
|
|
@@ -436,16 +526,16 @@ var UserModel = class {
|
|
|
436
526
|
[id]
|
|
437
527
|
);
|
|
438
528
|
}
|
|
439
|
-
async updateEmail(id,
|
|
529
|
+
async updateEmail(id, email2) {
|
|
440
530
|
await this.store.query(
|
|
441
531
|
`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,
|
|
442
|
-
[
|
|
532
|
+
[email2.toLowerCase().trim(), id]
|
|
443
533
|
);
|
|
444
534
|
}
|
|
445
|
-
async updatePhone(id,
|
|
535
|
+
async updatePhone(id, phone2) {
|
|
446
536
|
await this.store.query(
|
|
447
537
|
`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,
|
|
448
|
-
[
|
|
538
|
+
[phone2, id]
|
|
449
539
|
);
|
|
450
540
|
}
|
|
451
541
|
async updatePreferences(id, fields) {
|
|
@@ -478,14 +568,14 @@ var UserModel = class {
|
|
|
478
568
|
);
|
|
479
569
|
return row?.mfa_secret ?? null;
|
|
480
570
|
}
|
|
481
|
-
async upsertByProvider(
|
|
571
|
+
async upsertByProvider(email2, provider, providerId) {
|
|
482
572
|
const [row] = await this.store.query(
|
|
483
573
|
`INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
|
|
484
574
|
VALUES ($1, now(), $2, $3)
|
|
485
575
|
ON CONFLICT (email) DO UPDATE
|
|
486
576
|
SET provider = $2, provider_id = $3
|
|
487
577
|
RETURNING id`,
|
|
488
|
-
[
|
|
578
|
+
[email2, provider, providerId]
|
|
489
579
|
);
|
|
490
580
|
return row ?? null;
|
|
491
581
|
}
|
|
@@ -665,10 +755,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
665
755
|
{
|
|
666
756
|
status: 200,
|
|
667
757
|
headers: {
|
|
668
|
-
"Set-Cookie":
|
|
669
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
670
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
671
|
-
].join(", ")
|
|
758
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
672
759
|
}
|
|
673
760
|
}
|
|
674
761
|
);
|
|
@@ -742,11 +829,11 @@ function checkCooldown(lastSentAt, cooldownMs) {
|
|
|
742
829
|
init_password();
|
|
743
830
|
|
|
744
831
|
// src/services/email.ts
|
|
745
|
-
function normalizeEmail(
|
|
746
|
-
if (typeof
|
|
832
|
+
function normalizeEmail(email2) {
|
|
833
|
+
if (typeof email2 !== "string" || email2.length === 0) {
|
|
747
834
|
throw new Error("Invalid email");
|
|
748
835
|
}
|
|
749
|
-
const lower =
|
|
836
|
+
const lower = email2.trim().toLowerCase();
|
|
750
837
|
if (lower.length === 0) {
|
|
751
838
|
throw new Error("Email cannot be empty");
|
|
752
839
|
}
|
|
@@ -766,9 +853,9 @@ function normalizeEmail(email) {
|
|
|
766
853
|
}
|
|
767
854
|
return `${normalizedLocal}@${domain}`;
|
|
768
855
|
}
|
|
769
|
-
function normalizeEmailSafe(
|
|
856
|
+
function normalizeEmailSafe(email2) {
|
|
770
857
|
try {
|
|
771
|
-
return normalizeEmail(
|
|
858
|
+
return normalizeEmail(email2);
|
|
772
859
|
} catch {
|
|
773
860
|
return null;
|
|
774
861
|
}
|
|
@@ -869,13 +956,13 @@ var PhoneVerificationModel = class {
|
|
|
869
956
|
this.store = store;
|
|
870
957
|
}
|
|
871
958
|
store;
|
|
872
|
-
async upsert(userId,
|
|
959
|
+
async upsert(userId, phone2, otp, expiresAt) {
|
|
873
960
|
await this.store.query(
|
|
874
961
|
`INSERT INTO fonderie_phone_verifications (phone, user_id, otp, expires_at)
|
|
875
962
|
VALUES ($1, $2, $3, $4)
|
|
876
963
|
ON CONFLICT (phone) DO UPDATE
|
|
877
964
|
SET user_id = $2, otp = $3, expires_at = $4, created_at = now()`,
|
|
878
|
-
[
|
|
965
|
+
[phone2, userId, otp, expiresAt]
|
|
879
966
|
);
|
|
880
967
|
}
|
|
881
968
|
async findByUser(userId, otp) {
|
|
@@ -900,11 +987,11 @@ var PhoneVerificationModel = class {
|
|
|
900
987
|
};
|
|
901
988
|
|
|
902
989
|
// src/controllers/auth.controller.ts
|
|
903
|
-
function normalizePhone(
|
|
904
|
-
return
|
|
990
|
+
function normalizePhone(phone2) {
|
|
991
|
+
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
905
992
|
}
|
|
906
|
-
function isValidPhone(
|
|
907
|
-
return typeof
|
|
993
|
+
function isValidPhone(phone2) {
|
|
994
|
+
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone(phone2));
|
|
908
995
|
}
|
|
909
996
|
function extractRefreshToken(ctx) {
|
|
910
997
|
const body = ctx.meta["body"];
|
|
@@ -925,13 +1012,13 @@ function authController(store, config, bus) {
|
|
|
925
1012
|
return {
|
|
926
1013
|
register: async (ctx) => {
|
|
927
1014
|
const body = ctx.meta["body"];
|
|
928
|
-
const { email, password, phone, firstName = null, lastName = null } = body ?? {};
|
|
929
|
-
if (typeof
|
|
930
|
-
const normalizedEmail = normalizeEmailSafe(
|
|
1015
|
+
const { email: email2, password: password2, phone: phone2, firstName = null, lastName = null } = body ?? {};
|
|
1016
|
+
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
1017
|
+
const normalizedEmail = normalizeEmailSafe(email2);
|
|
931
1018
|
if (!normalizedEmail) {
|
|
932
1019
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
933
1020
|
}
|
|
934
|
-
if (
|
|
1021
|
+
if (password2.length < 8) {
|
|
935
1022
|
return (0, import_core4.setApiResponse)(
|
|
936
1023
|
import_core4.HTTP.UNPROCESSABLE,
|
|
937
1024
|
"INVALID_PARAMETER",
|
|
@@ -942,7 +1029,7 @@ function authController(store, config, bus) {
|
|
|
942
1029
|
if (existing) {
|
|
943
1030
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
944
1031
|
}
|
|
945
|
-
const passwordHash = await hashPassword(
|
|
1032
|
+
const passwordHash = await hashPassword(password2);
|
|
946
1033
|
const row = await users.create(
|
|
947
1034
|
normalizedEmail,
|
|
948
1035
|
passwordHash,
|
|
@@ -1002,27 +1089,24 @@ function authController(store, config, bus) {
|
|
|
1002
1089
|
{
|
|
1003
1090
|
status: 201,
|
|
1004
1091
|
headers: {
|
|
1005
|
-
"Set-Cookie":
|
|
1006
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1007
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1008
|
-
].join(", ")
|
|
1092
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1009
1093
|
}
|
|
1010
1094
|
}
|
|
1011
1095
|
);
|
|
1012
1096
|
}
|
|
1013
|
-
if (isValidPhone(
|
|
1014
|
-
const existing = await users.findByPhone(normalizePhone(
|
|
1097
|
+
if (isValidPhone(phone2)) {
|
|
1098
|
+
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
1015
1099
|
if (existing) {
|
|
1016
1100
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
1017
1101
|
}
|
|
1018
1102
|
const { id } = await users.findOrCreateByPhone(
|
|
1019
|
-
normalizePhone(
|
|
1103
|
+
normalizePhone(phone2),
|
|
1020
1104
|
firstName ?? null,
|
|
1021
1105
|
lastName ?? null
|
|
1022
1106
|
);
|
|
1023
1107
|
const otp = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
|
|
1024
1108
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1025
|
-
await phoneVerif.upsert(id, normalizePhone(
|
|
1109
|
+
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1026
1110
|
const user = await users.findById(id);
|
|
1027
1111
|
if (!user) {
|
|
1028
1112
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
@@ -1034,7 +1118,7 @@ function authController(store, config, bus) {
|
|
|
1034
1118
|
{
|
|
1035
1119
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1036
1120
|
data: { otp },
|
|
1037
|
-
recipient: { email: null, phone: normalizePhone(
|
|
1121
|
+
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1038
1122
|
},
|
|
1039
1123
|
reqOpts2
|
|
1040
1124
|
).catch(() => {
|
|
@@ -1067,10 +1151,7 @@ function authController(store, config, bus) {
|
|
|
1067
1151
|
{
|
|
1068
1152
|
status: 202,
|
|
1069
1153
|
headers: {
|
|
1070
|
-
"Set-Cookie":
|
|
1071
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1072
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1073
|
-
].join(", ")
|
|
1154
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1074
1155
|
}
|
|
1075
1156
|
}
|
|
1076
1157
|
);
|
|
@@ -1084,16 +1165,16 @@ function authController(store, config, bus) {
|
|
|
1084
1165
|
login: async (ctx) => {
|
|
1085
1166
|
const body = ctx.meta["body"];
|
|
1086
1167
|
if (typeof body?.["email"] === "string" && typeof body?.["password"] === "string") {
|
|
1087
|
-
const { email: rawEmail, password } = body;
|
|
1088
|
-
const
|
|
1089
|
-
if (!
|
|
1168
|
+
const { email: rawEmail, password: password2 } = body;
|
|
1169
|
+
const email2 = normalizeEmailSafe(rawEmail);
|
|
1170
|
+
if (!email2) {
|
|
1090
1171
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1091
1172
|
}
|
|
1092
|
-
const user = await users.findByEmail(
|
|
1173
|
+
const user = await users.findByEmail(email2);
|
|
1093
1174
|
if (!user || !user.passwordHash) {
|
|
1094
1175
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1095
1176
|
}
|
|
1096
|
-
const valid = await verifyPassword(
|
|
1177
|
+
const valid = await verifyPassword(password2, user.passwordHash);
|
|
1097
1178
|
if (!valid) {
|
|
1098
1179
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1099
1180
|
}
|
|
@@ -1129,17 +1210,14 @@ function authController(store, config, bus) {
|
|
|
1129
1210
|
{
|
|
1130
1211
|
status: 200,
|
|
1131
1212
|
headers: {
|
|
1132
|
-
"Set-Cookie":
|
|
1133
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1134
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1135
|
-
].join(", ")
|
|
1213
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1136
1214
|
}
|
|
1137
1215
|
}
|
|
1138
1216
|
);
|
|
1139
1217
|
}
|
|
1140
|
-
const
|
|
1141
|
-
if (isValidPhone(
|
|
1142
|
-
const user = await users.findByPhone(normalizePhone(
|
|
1218
|
+
const phone2 = body?.["phone"];
|
|
1219
|
+
if (isValidPhone(phone2)) {
|
|
1220
|
+
const user = await users.findByPhone(normalizePhone(phone2));
|
|
1143
1221
|
if (!user) {
|
|
1144
1222
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1145
1223
|
}
|
|
@@ -1152,11 +1230,11 @@ function authController(store, config, bus) {
|
|
|
1152
1230
|
}
|
|
1153
1231
|
const otp = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
|
|
1154
1232
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1155
|
-
await phoneVerif.upsert(user.id, normalizePhone(
|
|
1233
|
+
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1156
1234
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1157
1235
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1158
1236
|
data: { otp },
|
|
1159
|
-
recipient: { email: null, phone: normalizePhone(
|
|
1237
|
+
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1160
1238
|
}).catch(() => {
|
|
1161
1239
|
});
|
|
1162
1240
|
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
@@ -1175,10 +1253,7 @@ function authController(store, config, bus) {
|
|
|
1175
1253
|
{
|
|
1176
1254
|
status: 202,
|
|
1177
1255
|
headers: {
|
|
1178
|
-
"Set-Cookie":
|
|
1179
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1180
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1181
|
-
].join(", ")
|
|
1256
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1182
1257
|
}
|
|
1183
1258
|
}
|
|
1184
1259
|
);
|
|
@@ -1199,10 +1274,7 @@ function authController(store, config, bus) {
|
|
|
1199
1274
|
{
|
|
1200
1275
|
status: 200,
|
|
1201
1276
|
headers: {
|
|
1202
|
-
"Set-Cookie":
|
|
1203
|
-
"access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
|
|
1204
|
-
"refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
|
|
1205
|
-
].join(", ")
|
|
1277
|
+
"Set-Cookie": clearedTokenCookies(config)
|
|
1206
1278
|
}
|
|
1207
1279
|
}
|
|
1208
1280
|
);
|
|
@@ -1243,10 +1315,7 @@ function authController(store, config, bus) {
|
|
|
1243
1315
|
{
|
|
1244
1316
|
status: 200,
|
|
1245
1317
|
headers: {
|
|
1246
|
-
"Set-Cookie":
|
|
1247
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1248
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1249
|
-
].join(", ")
|
|
1318
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1250
1319
|
}
|
|
1251
1320
|
}
|
|
1252
1321
|
);
|
|
@@ -1257,11 +1326,11 @@ function authController(store, config, bus) {
|
|
|
1257
1326
|
if (typeof rawEmail !== "string") {
|
|
1258
1327
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
1259
1328
|
}
|
|
1260
|
-
const
|
|
1261
|
-
if (!
|
|
1329
|
+
const email2 = normalizeEmailSafe(rawEmail);
|
|
1330
|
+
if (!email2) {
|
|
1262
1331
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1263
1332
|
}
|
|
1264
|
-
const user = await users.findByEmail(
|
|
1333
|
+
const user = await users.findByEmail(email2);
|
|
1265
1334
|
if (!user) {
|
|
1266
1335
|
return (0, import_core4.setApiResponse)(
|
|
1267
1336
|
import_core4.HTTP.OK,
|
|
@@ -1274,12 +1343,9 @@ function authController(store, config, bus) {
|
|
|
1274
1343
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
1275
1344
|
if (remaining > 0) {
|
|
1276
1345
|
return (0, import_core4.setApiResponse)(
|
|
1277
|
-
import_core4.HTTP.
|
|
1278
|
-
"
|
|
1279
|
-
"
|
|
1280
|
-
{
|
|
1281
|
-
retryAfter: Math.ceil(remaining / 1e3)
|
|
1282
|
-
}
|
|
1346
|
+
import_core4.HTTP.OK,
|
|
1347
|
+
"PASSWORD_RESET_EMAIL_SENT",
|
|
1348
|
+
"Password reset email sent (if account exists)."
|
|
1283
1349
|
);
|
|
1284
1350
|
}
|
|
1285
1351
|
const pin = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
|
|
@@ -1287,7 +1353,7 @@ function authController(store, config, bus) {
|
|
|
1287
1353
|
await passwordReset.create(user.id, pin, expiresAt);
|
|
1288
1354
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1289
1355
|
type: MESSAGE_KEYS.passwordReset,
|
|
1290
|
-
recipient: { email, phone: null, deviceToken: null },
|
|
1356
|
+
recipient: { email: email2, phone: null, deviceToken: null },
|
|
1291
1357
|
data: { pin }
|
|
1292
1358
|
}).catch(() => {
|
|
1293
1359
|
});
|
|
@@ -1300,8 +1366,8 @@ function authController(store, config, bus) {
|
|
|
1300
1366
|
resetPassword: async (ctx) => {
|
|
1301
1367
|
const body = ctx.meta["body"];
|
|
1302
1368
|
const raw = body?.["pin"];
|
|
1303
|
-
const
|
|
1304
|
-
if (typeof raw !== "string" || typeof
|
|
1369
|
+
const password2 = body?.["password"];
|
|
1370
|
+
if (typeof raw !== "string" || typeof password2 !== "string") {
|
|
1305
1371
|
return (0, import_core4.setApiResponse)(
|
|
1306
1372
|
import_core4.HTTP.UNPROCESSABLE,
|
|
1307
1373
|
"INVALID_PARAMETER",
|
|
@@ -1315,7 +1381,7 @@ function authController(store, config, bus) {
|
|
|
1315
1381
|
"pin must be a 6-digit code"
|
|
1316
1382
|
);
|
|
1317
1383
|
}
|
|
1318
|
-
if (
|
|
1384
|
+
if (password2.length < 8) {
|
|
1319
1385
|
return (0, import_core4.setApiResponse)(
|
|
1320
1386
|
import_core4.HTTP.UNPROCESSABLE,
|
|
1321
1387
|
"INVALID_PARAMETER",
|
|
@@ -1327,7 +1393,7 @@ function authController(store, config, bus) {
|
|
|
1327
1393
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1328
1394
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "PASSWORD_RESET_FAILED", "Invalid or expired pin");
|
|
1329
1395
|
}
|
|
1330
|
-
const passwordHash = await hashPassword(
|
|
1396
|
+
const passwordHash = await hashPassword(password2);
|
|
1331
1397
|
await store.transaction(async (tx) => {
|
|
1332
1398
|
await Promise.all([
|
|
1333
1399
|
tx.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
|
|
@@ -1398,10 +1464,7 @@ function authController(store, config, bus) {
|
|
|
1398
1464
|
{
|
|
1399
1465
|
status: 200,
|
|
1400
1466
|
headers: {
|
|
1401
|
-
"Set-Cookie":
|
|
1402
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1403
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1404
|
-
].join(", ")
|
|
1467
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1405
1468
|
}
|
|
1406
1469
|
}
|
|
1407
1470
|
);
|
|
@@ -1434,8 +1497,8 @@ function authController(store, config, bus) {
|
|
|
1434
1497
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
1435
1498
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1436
1499
|
if (ctx.user.loginMethod === "phone") {
|
|
1437
|
-
const
|
|
1438
|
-
if (!
|
|
1500
|
+
const phone2 = ctx.user.phone;
|
|
1501
|
+
if (!phone2) {
|
|
1439
1502
|
return (0, import_core4.setApiResponse)(
|
|
1440
1503
|
import_core4.HTTP.BAD_REQUEST,
|
|
1441
1504
|
"NO_PHONE_ON_ACCOUNT",
|
|
@@ -1455,11 +1518,11 @@ function authController(store, config, bus) {
|
|
|
1455
1518
|
}
|
|
1456
1519
|
const otp = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
|
|
1457
1520
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
1458
|
-
await phoneVerif.upsert(ctx.user.id,
|
|
1521
|
+
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
1459
1522
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1460
1523
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1461
1524
|
data: { otp },
|
|
1462
|
-
recipient: { email: null, phone, deviceToken: null }
|
|
1525
|
+
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
1463
1526
|
}).catch(() => {
|
|
1464
1527
|
});
|
|
1465
1528
|
return (0, import_core4.setApiResponse)(
|
|
@@ -1512,13 +1575,13 @@ function authController(store, config, bus) {
|
|
|
1512
1575
|
var import_node_crypto3 = require("crypto");
|
|
1513
1576
|
var import_core5 = require("@fonderie/core");
|
|
1514
1577
|
var import_events3 = require("@fonderie/events");
|
|
1515
|
-
function normalizePhone2(
|
|
1516
|
-
return
|
|
1578
|
+
function normalizePhone2(phone2) {
|
|
1579
|
+
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
1517
1580
|
}
|
|
1518
|
-
function isValidPhone2(
|
|
1519
|
-
return typeof
|
|
1581
|
+
function isValidPhone2(phone2) {
|
|
1582
|
+
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone2));
|
|
1520
1583
|
}
|
|
1521
|
-
function userController(store, bus) {
|
|
1584
|
+
function userController(store, config, bus) {
|
|
1522
1585
|
const users = new UserModel(store);
|
|
1523
1586
|
const emailVerif = new EmailVerificationModel(store);
|
|
1524
1587
|
const phoneVerif = new PhoneVerificationModel(store);
|
|
@@ -1707,10 +1770,7 @@ function userController(store, bus) {
|
|
|
1707
1770
|
{
|
|
1708
1771
|
status: 200,
|
|
1709
1772
|
headers: {
|
|
1710
|
-
"Set-Cookie":
|
|
1711
|
-
"access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
|
|
1712
|
-
"refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
|
|
1713
|
-
].join(", ")
|
|
1773
|
+
"Set-Cookie": clearedTokenCookies(config)
|
|
1714
1774
|
}
|
|
1715
1775
|
}
|
|
1716
1776
|
);
|
|
@@ -1814,10 +1874,7 @@ function oauthController(store, config) {
|
|
|
1814
1874
|
{
|
|
1815
1875
|
status: 200,
|
|
1816
1876
|
headers: {
|
|
1817
|
-
"Set-Cookie":
|
|
1818
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1819
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1820
|
-
].join(", ")
|
|
1877
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1821
1878
|
}
|
|
1822
1879
|
}
|
|
1823
1880
|
);
|
|
@@ -1827,46 +1884,47 @@ function oauthController(store, config) {
|
|
|
1827
1884
|
|
|
1828
1885
|
// src/routes.ts
|
|
1829
1886
|
function buildAuthRoutes(store, config, bus) {
|
|
1830
|
-
const user = userController(store, bus);
|
|
1887
|
+
const user = userController(store, config, bus);
|
|
1831
1888
|
const auth = authController(store, config, bus);
|
|
1832
1889
|
const oauth = oauthController(store, config);
|
|
1833
1890
|
const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
|
|
1834
|
-
const verifyGate = config.requireVerification ?
|
|
1891
|
+
const verifyGate = config.requireVerification ? import_middlewares2.requireVerified : (_ctx, next) => next();
|
|
1835
1892
|
const routes = [
|
|
1836
1893
|
// Registration & Login (Public)
|
|
1837
|
-
["POST", "/auth/register", auth.register],
|
|
1838
|
-
["POST", "/auth/login", auth.login],
|
|
1894
|
+
["POST", "/auth/register", (0, import_middlewares.validate)(registerSchema), auth.register],
|
|
1895
|
+
["POST", "/auth/login", (0, import_middlewares.validate)(loginSchema), auth.login],
|
|
1839
1896
|
// Token Management (Public)
|
|
1840
|
-
["POST", "/auth/refresh", auth.refresh],
|
|
1897
|
+
["POST", "/auth/refresh", (0, import_middlewares.validate)(refreshSchema), auth.refresh],
|
|
1841
1898
|
// Email — Password Recovery (Public)
|
|
1842
|
-
["POST", "/auth/email/forgot", auth.forgotPassword],
|
|
1843
|
-
["POST", "/auth/email/reset", auth.resetPassword],
|
|
1899
|
+
["POST", "/auth/email/forgot", (0, import_middlewares.validate)(forgotPasswordSchema), auth.forgotPassword],
|
|
1900
|
+
["POST", "/auth/email/reset", (0, import_middlewares.validate)(resetPasswordSchema), auth.resetPassword],
|
|
1844
1901
|
// Verification (Protected — email or phone, determined by loginMethod)
|
|
1845
|
-
["POST", "/auth/verify",
|
|
1846
|
-
["GET", "/auth/send-verification",
|
|
1902
|
+
["POST", "/auth/verify", import_middlewares2.requireAuth, (0, import_middlewares.validate)(verifySchema), auth.verify],
|
|
1903
|
+
["GET", "/auth/send-verification", import_middlewares2.requireAuth, auth.sendVerification],
|
|
1847
1904
|
// Account Management (Protected)
|
|
1848
|
-
["POST", "/auth/logout",
|
|
1905
|
+
["POST", "/auth/logout", import_middlewares2.requireAuth, (0, import_middlewares.validate)(refreshSchema), auth.logout],
|
|
1849
1906
|
// User Profile (Protected; writes also gate on requireVerification)
|
|
1850
|
-
["GET", "/users",
|
|
1851
|
-
["PUT", "/users/profile",
|
|
1852
|
-
["PUT", "/users/preferences",
|
|
1853
|
-
["PUT", "/users/email",
|
|
1854
|
-
["PUT", "/users/phone",
|
|
1855
|
-
["PUT", "/users/password",
|
|
1856
|
-
["DELETE", "/users",
|
|
1907
|
+
["GET", "/users", import_middlewares2.requireAuth, user.me],
|
|
1908
|
+
["PUT", "/users/profile", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updateProfileSchema), user.updateProfile],
|
|
1909
|
+
["PUT", "/users/preferences", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updatePreferencesSchema), user.updatePreferences],
|
|
1910
|
+
["PUT", "/users/email", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updateEmailSchema), user.updateEmail],
|
|
1911
|
+
["PUT", "/users/phone", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updatePhoneSchema), user.updatePhone],
|
|
1912
|
+
["PUT", "/users/password", import_middlewares2.requireAuth, (0, import_middlewares.validate)(changePasswordSchema), user.changePassword],
|
|
1913
|
+
["DELETE", "/users", import_middlewares2.requireAuth, verifyGate, user.deleteMe],
|
|
1857
1914
|
// MFA (email sessions only — requireVerified is always enforced here
|
|
1858
1915
|
// because MFA is a security feature and email verification is meaningful)
|
|
1859
|
-
["POST", "/auth/mfa/setup",
|
|
1916
|
+
["POST", "/auth/mfa/setup", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, mfa.setup],
|
|
1860
1917
|
// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
|
|
1861
1918
|
// and full tokens (setup confirmation), so requireAnyAuth is used here.
|
|
1862
|
-
["POST", "/auth/mfa/verify",
|
|
1863
|
-
["POST", "/auth/mfa/disable",
|
|
1919
|
+
["POST", "/auth/mfa/verify", import_middlewares2.requireAnyAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.verify],
|
|
1920
|
+
["POST", "/auth/mfa/disable", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.disable],
|
|
1864
1921
|
[
|
|
1865
1922
|
"POST",
|
|
1866
1923
|
"/auth/mfa/backup-codes",
|
|
1867
|
-
|
|
1924
|
+
import_middlewares2.requireAuth,
|
|
1868
1925
|
requireEmailLogin,
|
|
1869
|
-
|
|
1926
|
+
import_middlewares2.requireVerified,
|
|
1927
|
+
(0, import_middlewares.validate)(mfaTokenSchema),
|
|
1870
1928
|
mfa.regenerateBackupCodes
|
|
1871
1929
|
]
|
|
1872
1930
|
];
|
|
@@ -1937,7 +1995,7 @@ var AuthModule = class {
|
|
|
1937
1995
|
};
|
|
1938
1996
|
|
|
1939
1997
|
// src/middlewares/require-auth.ts
|
|
1940
|
-
var
|
|
1998
|
+
var import_middlewares3 = require("@fonderie/core/middlewares");
|
|
1941
1999
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1942
2000
|
0 && (module.exports = {
|
|
1943
2001
|
AUTH_CONFIG_KEYS,
|
|
@@ -1946,7 +2004,9 @@ var import_middlewares2 = require("@fonderie/core/middlewares");
|
|
|
1946
2004
|
normalizeEmail,
|
|
1947
2005
|
normalizeEmailSafe,
|
|
1948
2006
|
requireAuth,
|
|
2007
|
+
schemas,
|
|
1949
2008
|
toUserDTO,
|
|
2009
|
+
validate,
|
|
1950
2010
|
withSession
|
|
1951
2011
|
});
|
|
1952
2012
|
//# sourceMappingURL=index.cjs.map
|