@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.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,74 @@ 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
|
+
|
|
84
154
|
// src/controllers/mfa.controller.ts
|
|
85
155
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
86
156
|
var import_core3 = require("@fonderie/core");
|
|
@@ -217,7 +287,7 @@ function base32Encode(buf) {
|
|
|
217
287
|
function generateTotpSecret() {
|
|
218
288
|
return base32Encode((0, import_node_crypto.randomBytes)(20));
|
|
219
289
|
}
|
|
220
|
-
function generateTotpUri(
|
|
290
|
+
function generateTotpUri(email2, secret, issuer) {
|
|
221
291
|
const params = new URLSearchParams({
|
|
222
292
|
secret,
|
|
223
293
|
issuer,
|
|
@@ -225,7 +295,7 @@ function generateTotpUri(email, secret, issuer) {
|
|
|
225
295
|
digits: String(DIGITS),
|
|
226
296
|
period: String(STEP)
|
|
227
297
|
});
|
|
228
|
-
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(
|
|
298
|
+
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email2)}?${params}`;
|
|
229
299
|
}
|
|
230
300
|
function verifyTotpToken(token, secret) {
|
|
231
301
|
const t = timeCounter();
|
|
@@ -317,21 +387,21 @@ var UserModel = class {
|
|
|
317
387
|
);
|
|
318
388
|
return row ?? null;
|
|
319
389
|
}
|
|
320
|
-
async findByEmail(
|
|
390
|
+
async findByEmail(email2) {
|
|
321
391
|
const [row] = await this.store.query(
|
|
322
392
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,
|
|
323
|
-
[
|
|
393
|
+
[email2]
|
|
324
394
|
);
|
|
325
395
|
return row ?? null;
|
|
326
396
|
}
|
|
327
|
-
async findByPhone(
|
|
397
|
+
async findByPhone(phone2) {
|
|
328
398
|
const [row] = await this.store.query(
|
|
329
399
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
|
|
330
|
-
[
|
|
400
|
+
[phone2]
|
|
331
401
|
);
|
|
332
402
|
return row ?? null;
|
|
333
403
|
}
|
|
334
|
-
async findOrCreateByPhone(
|
|
404
|
+
async findOrCreateByPhone(phone2, firstName = null, lastName = null) {
|
|
335
405
|
const [row] = await this.store.query(
|
|
336
406
|
`INSERT INTO fonderie_users (phone, first_name, last_name)
|
|
337
407
|
VALUES ($1, $2, $3)
|
|
@@ -340,16 +410,16 @@ var UserModel = class {
|
|
|
340
410
|
last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),
|
|
341
411
|
updated_at = now()
|
|
342
412
|
RETURNING id`,
|
|
343
|
-
[
|
|
413
|
+
[phone2, firstName, lastName]
|
|
344
414
|
);
|
|
345
415
|
return row;
|
|
346
416
|
}
|
|
347
|
-
async create(
|
|
417
|
+
async create(email2, passwordHash, firstName, lastName) {
|
|
348
418
|
const [row] = await this.store.query(
|
|
349
419
|
`INSERT INTO fonderie_users (email, password_hash, first_name, last_name)
|
|
350
420
|
VALUES ($1, $2, $3, $4)
|
|
351
421
|
RETURNING id`,
|
|
352
|
-
[
|
|
422
|
+
[email2.toLowerCase().trim(), passwordHash, firstName, lastName]
|
|
353
423
|
);
|
|
354
424
|
return row ?? null;
|
|
355
425
|
}
|
|
@@ -436,16 +506,16 @@ var UserModel = class {
|
|
|
436
506
|
[id]
|
|
437
507
|
);
|
|
438
508
|
}
|
|
439
|
-
async updateEmail(id,
|
|
509
|
+
async updateEmail(id, email2) {
|
|
440
510
|
await this.store.query(
|
|
441
511
|
`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,
|
|
442
|
-
[
|
|
512
|
+
[email2.toLowerCase().trim(), id]
|
|
443
513
|
);
|
|
444
514
|
}
|
|
445
|
-
async updatePhone(id,
|
|
515
|
+
async updatePhone(id, phone2) {
|
|
446
516
|
await this.store.query(
|
|
447
517
|
`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,
|
|
448
|
-
[
|
|
518
|
+
[phone2, id]
|
|
449
519
|
);
|
|
450
520
|
}
|
|
451
521
|
async updatePreferences(id, fields) {
|
|
@@ -478,14 +548,14 @@ var UserModel = class {
|
|
|
478
548
|
);
|
|
479
549
|
return row?.mfa_secret ?? null;
|
|
480
550
|
}
|
|
481
|
-
async upsertByProvider(
|
|
551
|
+
async upsertByProvider(email2, provider, providerId) {
|
|
482
552
|
const [row] = await this.store.query(
|
|
483
553
|
`INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
|
|
484
554
|
VALUES ($1, now(), $2, $3)
|
|
485
555
|
ON CONFLICT (email) DO UPDATE
|
|
486
556
|
SET provider = $2, provider_id = $3
|
|
487
557
|
RETURNING id`,
|
|
488
|
-
[
|
|
558
|
+
[email2, provider, providerId]
|
|
489
559
|
);
|
|
490
560
|
return row ?? null;
|
|
491
561
|
}
|
|
@@ -742,11 +812,11 @@ function checkCooldown(lastSentAt, cooldownMs) {
|
|
|
742
812
|
init_password();
|
|
743
813
|
|
|
744
814
|
// src/services/email.ts
|
|
745
|
-
function normalizeEmail(
|
|
746
|
-
if (typeof
|
|
815
|
+
function normalizeEmail(email2) {
|
|
816
|
+
if (typeof email2 !== "string" || email2.length === 0) {
|
|
747
817
|
throw new Error("Invalid email");
|
|
748
818
|
}
|
|
749
|
-
const lower =
|
|
819
|
+
const lower = email2.trim().toLowerCase();
|
|
750
820
|
if (lower.length === 0) {
|
|
751
821
|
throw new Error("Email cannot be empty");
|
|
752
822
|
}
|
|
@@ -766,9 +836,9 @@ function normalizeEmail(email) {
|
|
|
766
836
|
}
|
|
767
837
|
return `${normalizedLocal}@${domain}`;
|
|
768
838
|
}
|
|
769
|
-
function normalizeEmailSafe(
|
|
839
|
+
function normalizeEmailSafe(email2) {
|
|
770
840
|
try {
|
|
771
|
-
return normalizeEmail(
|
|
841
|
+
return normalizeEmail(email2);
|
|
772
842
|
} catch {
|
|
773
843
|
return null;
|
|
774
844
|
}
|
|
@@ -869,13 +939,13 @@ var PhoneVerificationModel = class {
|
|
|
869
939
|
this.store = store;
|
|
870
940
|
}
|
|
871
941
|
store;
|
|
872
|
-
async upsert(userId,
|
|
942
|
+
async upsert(userId, phone2, otp, expiresAt) {
|
|
873
943
|
await this.store.query(
|
|
874
944
|
`INSERT INTO fonderie_phone_verifications (phone, user_id, otp, expires_at)
|
|
875
945
|
VALUES ($1, $2, $3, $4)
|
|
876
946
|
ON CONFLICT (phone) DO UPDATE
|
|
877
947
|
SET user_id = $2, otp = $3, expires_at = $4, created_at = now()`,
|
|
878
|
-
[
|
|
948
|
+
[phone2, userId, otp, expiresAt]
|
|
879
949
|
);
|
|
880
950
|
}
|
|
881
951
|
async findByUser(userId, otp) {
|
|
@@ -900,11 +970,11 @@ var PhoneVerificationModel = class {
|
|
|
900
970
|
};
|
|
901
971
|
|
|
902
972
|
// src/controllers/auth.controller.ts
|
|
903
|
-
function normalizePhone(
|
|
904
|
-
return
|
|
973
|
+
function normalizePhone(phone2) {
|
|
974
|
+
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
905
975
|
}
|
|
906
|
-
function isValidPhone(
|
|
907
|
-
return typeof
|
|
976
|
+
function isValidPhone(phone2) {
|
|
977
|
+
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone(phone2));
|
|
908
978
|
}
|
|
909
979
|
function extractRefreshToken(ctx) {
|
|
910
980
|
const body = ctx.meta["body"];
|
|
@@ -925,13 +995,13 @@ function authController(store, config, bus) {
|
|
|
925
995
|
return {
|
|
926
996
|
register: async (ctx) => {
|
|
927
997
|
const body = ctx.meta["body"];
|
|
928
|
-
const { email, password, phone, firstName = null, lastName = null } = body ?? {};
|
|
929
|
-
if (typeof
|
|
930
|
-
const normalizedEmail = normalizeEmailSafe(
|
|
998
|
+
const { email: email2, password: password2, phone: phone2, firstName = null, lastName = null } = body ?? {};
|
|
999
|
+
if (typeof email2 === "string" && typeof password2 === "string") {
|
|
1000
|
+
const normalizedEmail = normalizeEmailSafe(email2);
|
|
931
1001
|
if (!normalizedEmail) {
|
|
932
1002
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
933
1003
|
}
|
|
934
|
-
if (
|
|
1004
|
+
if (password2.length < 8) {
|
|
935
1005
|
return (0, import_core4.setApiResponse)(
|
|
936
1006
|
import_core4.HTTP.UNPROCESSABLE,
|
|
937
1007
|
"INVALID_PARAMETER",
|
|
@@ -942,7 +1012,7 @@ function authController(store, config, bus) {
|
|
|
942
1012
|
if (existing) {
|
|
943
1013
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
|
|
944
1014
|
}
|
|
945
|
-
const passwordHash = await hashPassword(
|
|
1015
|
+
const passwordHash = await hashPassword(password2);
|
|
946
1016
|
const row = await users.create(
|
|
947
1017
|
normalizedEmail,
|
|
948
1018
|
passwordHash,
|
|
@@ -1010,19 +1080,19 @@ function authController(store, config, bus) {
|
|
|
1010
1080
|
}
|
|
1011
1081
|
);
|
|
1012
1082
|
}
|
|
1013
|
-
if (isValidPhone(
|
|
1014
|
-
const existing = await users.findByPhone(normalizePhone(
|
|
1083
|
+
if (isValidPhone(phone2)) {
|
|
1084
|
+
const existing = await users.findByPhone(normalizePhone(phone2));
|
|
1015
1085
|
if (existing) {
|
|
1016
1086
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
|
|
1017
1087
|
}
|
|
1018
1088
|
const { id } = await users.findOrCreateByPhone(
|
|
1019
|
-
normalizePhone(
|
|
1089
|
+
normalizePhone(phone2),
|
|
1020
1090
|
firstName ?? null,
|
|
1021
1091
|
lastName ?? null
|
|
1022
1092
|
);
|
|
1023
1093
|
const otp = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
|
|
1024
1094
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1025
|
-
await phoneVerif.upsert(id, normalizePhone(
|
|
1095
|
+
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1026
1096
|
const user = await users.findById(id);
|
|
1027
1097
|
if (!user) {
|
|
1028
1098
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
@@ -1034,7 +1104,7 @@ function authController(store, config, bus) {
|
|
|
1034
1104
|
{
|
|
1035
1105
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1036
1106
|
data: { otp },
|
|
1037
|
-
recipient: { email: null, phone: normalizePhone(
|
|
1107
|
+
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1038
1108
|
},
|
|
1039
1109
|
reqOpts2
|
|
1040
1110
|
).catch(() => {
|
|
@@ -1084,16 +1154,16 @@ function authController(store, config, bus) {
|
|
|
1084
1154
|
login: async (ctx) => {
|
|
1085
1155
|
const body = ctx.meta["body"];
|
|
1086
1156
|
if (typeof body?.["email"] === "string" && typeof body?.["password"] === "string") {
|
|
1087
|
-
const { email: rawEmail, password } = body;
|
|
1088
|
-
const
|
|
1089
|
-
if (!
|
|
1157
|
+
const { email: rawEmail, password: password2 } = body;
|
|
1158
|
+
const email2 = normalizeEmailSafe(rawEmail);
|
|
1159
|
+
if (!email2) {
|
|
1090
1160
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1091
1161
|
}
|
|
1092
|
-
const user = await users.findByEmail(
|
|
1162
|
+
const user = await users.findByEmail(email2);
|
|
1093
1163
|
if (!user || !user.passwordHash) {
|
|
1094
1164
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1095
1165
|
}
|
|
1096
|
-
const valid = await verifyPassword(
|
|
1166
|
+
const valid = await verifyPassword(password2, user.passwordHash);
|
|
1097
1167
|
if (!valid) {
|
|
1098
1168
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1099
1169
|
}
|
|
@@ -1137,9 +1207,9 @@ function authController(store, config, bus) {
|
|
|
1137
1207
|
}
|
|
1138
1208
|
);
|
|
1139
1209
|
}
|
|
1140
|
-
const
|
|
1141
|
-
if (isValidPhone(
|
|
1142
|
-
const user = await users.findByPhone(normalizePhone(
|
|
1210
|
+
const phone2 = body?.["phone"];
|
|
1211
|
+
if (isValidPhone(phone2)) {
|
|
1212
|
+
const user = await users.findByPhone(normalizePhone(phone2));
|
|
1143
1213
|
if (!user) {
|
|
1144
1214
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1145
1215
|
}
|
|
@@ -1152,11 +1222,11 @@ function authController(store, config, bus) {
|
|
|
1152
1222
|
}
|
|
1153
1223
|
const otp = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
|
|
1154
1224
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1155
|
-
await phoneVerif.upsert(user.id, normalizePhone(
|
|
1225
|
+
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1156
1226
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1157
1227
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1158
1228
|
data: { otp },
|
|
1159
|
-
recipient: { email: null, phone: normalizePhone(
|
|
1229
|
+
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1160
1230
|
}).catch(() => {
|
|
1161
1231
|
});
|
|
1162
1232
|
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
@@ -1257,11 +1327,11 @@ function authController(store, config, bus) {
|
|
|
1257
1327
|
if (typeof rawEmail !== "string") {
|
|
1258
1328
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
1259
1329
|
}
|
|
1260
|
-
const
|
|
1261
|
-
if (!
|
|
1330
|
+
const email2 = normalizeEmailSafe(rawEmail);
|
|
1331
|
+
if (!email2) {
|
|
1262
1332
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
|
|
1263
1333
|
}
|
|
1264
|
-
const user = await users.findByEmail(
|
|
1334
|
+
const user = await users.findByEmail(email2);
|
|
1265
1335
|
if (!user) {
|
|
1266
1336
|
return (0, import_core4.setApiResponse)(
|
|
1267
1337
|
import_core4.HTTP.OK,
|
|
@@ -1287,7 +1357,7 @@ function authController(store, config, bus) {
|
|
|
1287
1357
|
await passwordReset.create(user.id, pin, expiresAt);
|
|
1288
1358
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1289
1359
|
type: MESSAGE_KEYS.passwordReset,
|
|
1290
|
-
recipient: { email, phone: null, deviceToken: null },
|
|
1360
|
+
recipient: { email: email2, phone: null, deviceToken: null },
|
|
1291
1361
|
data: { pin }
|
|
1292
1362
|
}).catch(() => {
|
|
1293
1363
|
});
|
|
@@ -1300,8 +1370,8 @@ function authController(store, config, bus) {
|
|
|
1300
1370
|
resetPassword: async (ctx) => {
|
|
1301
1371
|
const body = ctx.meta["body"];
|
|
1302
1372
|
const raw = body?.["pin"];
|
|
1303
|
-
const
|
|
1304
|
-
if (typeof raw !== "string" || typeof
|
|
1373
|
+
const password2 = body?.["password"];
|
|
1374
|
+
if (typeof raw !== "string" || typeof password2 !== "string") {
|
|
1305
1375
|
return (0, import_core4.setApiResponse)(
|
|
1306
1376
|
import_core4.HTTP.UNPROCESSABLE,
|
|
1307
1377
|
"INVALID_PARAMETER",
|
|
@@ -1315,7 +1385,7 @@ function authController(store, config, bus) {
|
|
|
1315
1385
|
"pin must be a 6-digit code"
|
|
1316
1386
|
);
|
|
1317
1387
|
}
|
|
1318
|
-
if (
|
|
1388
|
+
if (password2.length < 8) {
|
|
1319
1389
|
return (0, import_core4.setApiResponse)(
|
|
1320
1390
|
import_core4.HTTP.UNPROCESSABLE,
|
|
1321
1391
|
"INVALID_PARAMETER",
|
|
@@ -1327,7 +1397,7 @@ function authController(store, config, bus) {
|
|
|
1327
1397
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1328
1398
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "PASSWORD_RESET_FAILED", "Invalid or expired pin");
|
|
1329
1399
|
}
|
|
1330
|
-
const passwordHash = await hashPassword(
|
|
1400
|
+
const passwordHash = await hashPassword(password2);
|
|
1331
1401
|
await store.transaction(async (tx) => {
|
|
1332
1402
|
await Promise.all([
|
|
1333
1403
|
tx.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
|
|
@@ -1434,8 +1504,8 @@ function authController(store, config, bus) {
|
|
|
1434
1504
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
1435
1505
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1436
1506
|
if (ctx.user.loginMethod === "phone") {
|
|
1437
|
-
const
|
|
1438
|
-
if (!
|
|
1507
|
+
const phone2 = ctx.user.phone;
|
|
1508
|
+
if (!phone2) {
|
|
1439
1509
|
return (0, import_core4.setApiResponse)(
|
|
1440
1510
|
import_core4.HTTP.BAD_REQUEST,
|
|
1441
1511
|
"NO_PHONE_ON_ACCOUNT",
|
|
@@ -1455,11 +1525,11 @@ function authController(store, config, bus) {
|
|
|
1455
1525
|
}
|
|
1456
1526
|
const otp = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
|
|
1457
1527
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
1458
|
-
await phoneVerif.upsert(ctx.user.id,
|
|
1528
|
+
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
1459
1529
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1460
1530
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1461
1531
|
data: { otp },
|
|
1462
|
-
recipient: { email: null, phone, deviceToken: null }
|
|
1532
|
+
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
1463
1533
|
}).catch(() => {
|
|
1464
1534
|
});
|
|
1465
1535
|
return (0, import_core4.setApiResponse)(
|
|
@@ -1512,11 +1582,11 @@ function authController(store, config, bus) {
|
|
|
1512
1582
|
var import_node_crypto3 = require("crypto");
|
|
1513
1583
|
var import_core5 = require("@fonderie/core");
|
|
1514
1584
|
var import_events3 = require("@fonderie/events");
|
|
1515
|
-
function normalizePhone2(
|
|
1516
|
-
return
|
|
1585
|
+
function normalizePhone2(phone2) {
|
|
1586
|
+
return phone2.trim().replace(/[\s()\-\.]/g, "");
|
|
1517
1587
|
}
|
|
1518
|
-
function isValidPhone2(
|
|
1519
|
-
return typeof
|
|
1588
|
+
function isValidPhone2(phone2) {
|
|
1589
|
+
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone2));
|
|
1520
1590
|
}
|
|
1521
1591
|
function userController(store, bus) {
|
|
1522
1592
|
const users = new UserModel(store);
|
|
@@ -1831,42 +1901,43 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1831
1901
|
const auth = authController(store, config, bus);
|
|
1832
1902
|
const oauth = oauthController(store, config);
|
|
1833
1903
|
const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
|
|
1834
|
-
const verifyGate = config.requireVerification ?
|
|
1904
|
+
const verifyGate = config.requireVerification ? import_middlewares2.requireVerified : (_ctx, next) => next();
|
|
1835
1905
|
const routes = [
|
|
1836
1906
|
// Registration & Login (Public)
|
|
1837
|
-
["POST", "/auth/register", auth.register],
|
|
1838
|
-
["POST", "/auth/login", auth.login],
|
|
1907
|
+
["POST", "/auth/register", (0, import_middlewares.validate)(registerSchema), auth.register],
|
|
1908
|
+
["POST", "/auth/login", (0, import_middlewares.validate)(loginSchema), auth.login],
|
|
1839
1909
|
// Token Management (Public)
|
|
1840
|
-
["POST", "/auth/refresh", auth.refresh],
|
|
1910
|
+
["POST", "/auth/refresh", (0, import_middlewares.validate)(refreshSchema), auth.refresh],
|
|
1841
1911
|
// Email — Password Recovery (Public)
|
|
1842
|
-
["POST", "/auth/email/forgot", auth.forgotPassword],
|
|
1843
|
-
["POST", "/auth/email/reset", auth.resetPassword],
|
|
1912
|
+
["POST", "/auth/email/forgot", (0, import_middlewares.validate)(forgotPasswordSchema), auth.forgotPassword],
|
|
1913
|
+
["POST", "/auth/email/reset", (0, import_middlewares.validate)(resetPasswordSchema), auth.resetPassword],
|
|
1844
1914
|
// Verification (Protected — email or phone, determined by loginMethod)
|
|
1845
|
-
["POST", "/auth/verify",
|
|
1846
|
-
["GET", "/auth/send-verification",
|
|
1915
|
+
["POST", "/auth/verify", import_middlewares2.requireAuth, (0, import_middlewares.validate)(verifySchema), auth.verify],
|
|
1916
|
+
["GET", "/auth/send-verification", import_middlewares2.requireAuth, auth.sendVerification],
|
|
1847
1917
|
// Account Management (Protected)
|
|
1848
|
-
["POST", "/auth/logout",
|
|
1918
|
+
["POST", "/auth/logout", import_middlewares2.requireAuth, (0, import_middlewares.validate)(refreshSchema), auth.logout],
|
|
1849
1919
|
// 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",
|
|
1920
|
+
["GET", "/users", import_middlewares2.requireAuth, user.me],
|
|
1921
|
+
["PUT", "/users/profile", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updateProfileSchema), user.updateProfile],
|
|
1922
|
+
["PUT", "/users/preferences", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updatePreferencesSchema), user.updatePreferences],
|
|
1923
|
+
["PUT", "/users/email", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updateEmailSchema), user.updateEmail],
|
|
1924
|
+
["PUT", "/users/phone", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updatePhoneSchema), user.updatePhone],
|
|
1925
|
+
["PUT", "/users/password", import_middlewares2.requireAuth, (0, import_middlewares.validate)(changePasswordSchema), user.changePassword],
|
|
1926
|
+
["DELETE", "/users", import_middlewares2.requireAuth, verifyGate, user.deleteMe],
|
|
1857
1927
|
// MFA (email sessions only — requireVerified is always enforced here
|
|
1858
1928
|
// because MFA is a security feature and email verification is meaningful)
|
|
1859
|
-
["POST", "/auth/mfa/setup",
|
|
1929
|
+
["POST", "/auth/mfa/setup", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, mfa.setup],
|
|
1860
1930
|
// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
|
|
1861
1931
|
// and full tokens (setup confirmation), so requireAnyAuth is used here.
|
|
1862
|
-
["POST", "/auth/mfa/verify",
|
|
1863
|
-
["POST", "/auth/mfa/disable",
|
|
1932
|
+
["POST", "/auth/mfa/verify", import_middlewares2.requireAnyAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.verify],
|
|
1933
|
+
["POST", "/auth/mfa/disable", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.disable],
|
|
1864
1934
|
[
|
|
1865
1935
|
"POST",
|
|
1866
1936
|
"/auth/mfa/backup-codes",
|
|
1867
|
-
|
|
1937
|
+
import_middlewares2.requireAuth,
|
|
1868
1938
|
requireEmailLogin,
|
|
1869
|
-
|
|
1939
|
+
import_middlewares2.requireVerified,
|
|
1940
|
+
(0, import_middlewares.validate)(mfaTokenSchema),
|
|
1870
1941
|
mfa.regenerateBackupCodes
|
|
1871
1942
|
]
|
|
1872
1943
|
];
|
|
@@ -1937,7 +2008,7 @@ var AuthModule = class {
|
|
|
1937
2008
|
};
|
|
1938
2009
|
|
|
1939
2010
|
// src/middlewares/require-auth.ts
|
|
1940
|
-
var
|
|
2011
|
+
var import_middlewares3 = require("@fonderie/core/middlewares");
|
|
1941
2012
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1942
2013
|
0 && (module.exports = {
|
|
1943
2014
|
AUTH_CONFIG_KEYS,
|
|
@@ -1946,7 +2017,9 @@ var import_middlewares2 = require("@fonderie/core/middlewares");
|
|
|
1946
2017
|
normalizeEmail,
|
|
1947
2018
|
normalizeEmailSafe,
|
|
1948
2019
|
requireAuth,
|
|
2020
|
+
schemas,
|
|
1949
2021
|
toUserDTO,
|
|
2022
|
+
validate,
|
|
1950
2023
|
withSession
|
|
1951
2024
|
});
|
|
1952
2025
|
//# sourceMappingURL=index.cjs.map
|