aaspai-authx 0.1.9 → 0.2.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/LICENSE +2 -2
- package/dist/express/index.cjs +152 -38
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.js +152 -38
- package/dist/express/index.js.map +1 -1
- package/dist/index.cjs +152 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +152 -38
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +152 -38
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.js +152 -38
- package/dist/nest/index.js.map +1 -1
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
3
|
(See standard MIT text)
|
package/dist/express/index.cjs
CHANGED
|
@@ -261,7 +261,18 @@ var OrgUserSchema = new import_mongoose2.default.Schema(
|
|
|
261
261
|
lastEmailSent: { type: [Date], default: [] },
|
|
262
262
|
lastPasswordReset: { type: Date },
|
|
263
263
|
metadata: { type: [MetadataSchema], default: [] },
|
|
264
|
-
passwordHash: { type: String }
|
|
264
|
+
passwordHash: { type: String },
|
|
265
|
+
/**
|
|
266
|
+
* Optional auth provider list to distinguish how the user is allowed to authenticate.
|
|
267
|
+
* Example values: ['password'], ['google'], ['password', 'google'].
|
|
268
|
+
* This lets us enforce correct behavior when users mix email/password and Google login.
|
|
269
|
+
*/
|
|
270
|
+
providers: { type: [String], default: void 0 },
|
|
271
|
+
/**
|
|
272
|
+
* Optional provider-specific IDs for linking external accounts.
|
|
273
|
+
*/
|
|
274
|
+
googleId: { type: String },
|
|
275
|
+
githubId: { type: Number }
|
|
265
276
|
},
|
|
266
277
|
{ timestamps: true, collection: "users" }
|
|
267
278
|
);
|
|
@@ -418,8 +429,8 @@ function isPasswordStrong(v) {
|
|
|
418
429
|
}
|
|
419
430
|
function validateSignup(req, res, next) {
|
|
420
431
|
const { firstName, lastName, email, password, projectId, metadata } = req.body || {};
|
|
421
|
-
if (!firstName
|
|
422
|
-
return res.status(400).json({ error: "firstName
|
|
432
|
+
if (!firstName)
|
|
433
|
+
return res.status(400).json({ error: "firstName required" });
|
|
423
434
|
if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
|
|
424
435
|
if (!isPasswordStrong(password))
|
|
425
436
|
return res.status(400).json({ error: "weak password" });
|
|
@@ -447,6 +458,25 @@ function validateResendEmail(req, res, next) {
|
|
|
447
458
|
if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
|
|
448
459
|
next();
|
|
449
460
|
}
|
|
461
|
+
function validateProfileUpdate(req, res, next) {
|
|
462
|
+
const { firstName, lastName } = req.body || {};
|
|
463
|
+
if (firstName == null && lastName == null) {
|
|
464
|
+
return res.status(400).json({ error: "firstName or lastName required" });
|
|
465
|
+
}
|
|
466
|
+
if (firstName !== void 0 && typeof firstName !== "string") {
|
|
467
|
+
return res.status(400).json({ error: "firstName must be string" });
|
|
468
|
+
}
|
|
469
|
+
if (lastName !== void 0 && typeof lastName !== "string") {
|
|
470
|
+
return res.status(400).json({ error: "lastName must be string" });
|
|
471
|
+
}
|
|
472
|
+
if (typeof firstName === "string" && !firstName.trim()) {
|
|
473
|
+
return res.status(400).json({ error: "firstName required" });
|
|
474
|
+
}
|
|
475
|
+
if (typeof lastName === "string" && !lastName.trim()) {
|
|
476
|
+
return res.status(400).json({ error: "lastName required" });
|
|
477
|
+
}
|
|
478
|
+
next();
|
|
479
|
+
}
|
|
450
480
|
function validateSendInvite(req, res, next) {
|
|
451
481
|
const { email, role } = req.body || {};
|
|
452
482
|
if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
|
|
@@ -1049,6 +1079,10 @@ function buildResetPasswordEmailTemplate(data) {
|
|
|
1049
1079
|
}
|
|
1050
1080
|
|
|
1051
1081
|
// src/express/auth.routes.ts
|
|
1082
|
+
var EMAIL_VERIFICATION_TTL_SEC = 24 * 60 * 60;
|
|
1083
|
+
var EMAIL_VERIFICATION_EXPIRES_IN = "24 hours";
|
|
1084
|
+
var PASSWORD_RESET_TTL_SEC = 24 * 60 * 60;
|
|
1085
|
+
var PASSWORD_RESET_EXPIRES_IN = "24 hours";
|
|
1052
1086
|
function createAuthRouter(options = {}) {
|
|
1053
1087
|
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
|
1054
1088
|
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
@@ -1080,13 +1114,23 @@ function createAuthRouter(options = {}) {
|
|
|
1080
1114
|
r.post("/login", validateLogin, async (req, res) => {
|
|
1081
1115
|
const { email: emailAddress, password } = req.body || {};
|
|
1082
1116
|
try {
|
|
1083
|
-
const
|
|
1117
|
+
const userDoc = await OrgUser.findOne({ email: emailAddress }).select(
|
|
1118
|
+
"+password"
|
|
1119
|
+
);
|
|
1120
|
+
const user = userDoc?.toObject();
|
|
1084
1121
|
if (!user) {
|
|
1085
1122
|
return res.status(400).json({
|
|
1086
1123
|
error: "Invalid email or password",
|
|
1087
1124
|
code: "INVALID_CREDENTIALS"
|
|
1088
1125
|
});
|
|
1089
1126
|
}
|
|
1127
|
+
const providers = user.providers;
|
|
1128
|
+
if (Array.isArray(providers) && providers.includes("google") && !providers.includes("password")) {
|
|
1129
|
+
return res.status(400).json({
|
|
1130
|
+
error: "This account is configured for Google login. Please continue with Google or link a password in your account settings.",
|
|
1131
|
+
code: "OAUTH_ONLY_ACCOUNT"
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1090
1134
|
if (!user.emailVerified) {
|
|
1091
1135
|
return res.status(400).json({
|
|
1092
1136
|
error: "Please verify your email before logging in.",
|
|
@@ -1147,7 +1191,10 @@ function createAuthRouter(options = {}) {
|
|
|
1147
1191
|
projectId,
|
|
1148
1192
|
metadata,
|
|
1149
1193
|
roles: ["platform_user"],
|
|
1150
|
-
emailVerified: false
|
|
1194
|
+
emailVerified: false,
|
|
1195
|
+
// Mark that this user can log in with password. This will be used
|
|
1196
|
+
// to enforce correct behavior when mixing with Google login.
|
|
1197
|
+
providers: ["password"]
|
|
1151
1198
|
},
|
|
1152
1199
|
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
|
1153
1200
|
);
|
|
@@ -1165,9 +1212,10 @@ function createAuthRouter(options = {}) {
|
|
|
1165
1212
|
{
|
|
1166
1213
|
userId: user.id,
|
|
1167
1214
|
email: user.email
|
|
1168
|
-
}
|
|
1215
|
+
},
|
|
1216
|
+
EMAIL_VERIFICATION_TTL_SEC
|
|
1169
1217
|
)}`,
|
|
1170
|
-
expiresIn:
|
|
1218
|
+
expiresIn: EMAIL_VERIFICATION_EXPIRES_IN
|
|
1171
1219
|
}),
|
|
1172
1220
|
from: COMPANY_NAME
|
|
1173
1221
|
});
|
|
@@ -1190,6 +1238,33 @@ function createAuthRouter(options = {}) {
|
|
|
1190
1238
|
r.get("/me", requireAuth(), (req, res) => {
|
|
1191
1239
|
return res.json(req.user || null);
|
|
1192
1240
|
});
|
|
1241
|
+
r.put("/me", requireAuth(), validateProfileUpdate, async (req, res) => {
|
|
1242
|
+
const session = req.user;
|
|
1243
|
+
if (!session?.userId) {
|
|
1244
|
+
return res.status(401).json({ ok: false, error: "Unauthorized" });
|
|
1245
|
+
}
|
|
1246
|
+
const { firstName, lastName } = req.body || {};
|
|
1247
|
+
const updates = {};
|
|
1248
|
+
if (typeof firstName === "string") updates.firstName = firstName.trim();
|
|
1249
|
+
if (typeof lastName === "string") updates.lastName = lastName.trim();
|
|
1250
|
+
try {
|
|
1251
|
+
const userDoc = await OrgUser.findOneAndUpdate(
|
|
1252
|
+
{ id: session.userId },
|
|
1253
|
+
{ $set: updates },
|
|
1254
|
+
{ new: true }
|
|
1255
|
+
);
|
|
1256
|
+
if (!userDoc) {
|
|
1257
|
+
return res.status(404).json({ ok: false, error: "User not found" });
|
|
1258
|
+
}
|
|
1259
|
+
const user = userDoc.toObject();
|
|
1260
|
+
const tokens = generateTokens(user);
|
|
1261
|
+
setAuthCookies(res, tokens, cookieConfig);
|
|
1262
|
+
return res.json({ ok: true, user: toUserResponse(user) });
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
console.error("Update profile error:", err);
|
|
1265
|
+
return res.status(500).json({ ok: false, error: "Internal server error" });
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1193
1268
|
r.post("/logout", async (_req, res) => {
|
|
1194
1269
|
const clearOptions = buildClearCookieOptions(cookieConfig);
|
|
1195
1270
|
res.clearCookie("access_token", clearOptions);
|
|
@@ -1245,7 +1320,7 @@ function createAuthRouter(options = {}) {
|
|
|
1245
1320
|
const token = email.sign({
|
|
1246
1321
|
email: user.email,
|
|
1247
1322
|
userId: user.id
|
|
1248
|
-
});
|
|
1323
|
+
}, EMAIL_VERIFICATION_TTL_SEC);
|
|
1249
1324
|
const resendResult = await sendRateLimitedEmail({
|
|
1250
1325
|
emailService: email,
|
|
1251
1326
|
user,
|
|
@@ -1253,13 +1328,8 @@ function createAuthRouter(options = {}) {
|
|
|
1253
1328
|
// html: buildVerificationTemplate(token, options),
|
|
1254
1329
|
html: buildVerificationEmailTemplate({
|
|
1255
1330
|
firstName: user.firstName,
|
|
1256
|
-
verificationUrl: `${getFrontendBaseUrl(options)}/verify-email?token=${
|
|
1257
|
-
|
|
1258
|
-
userId: user.id,
|
|
1259
|
-
email: user.email
|
|
1260
|
-
}
|
|
1261
|
-
)}`,
|
|
1262
|
-
expiresIn: "1 hour"
|
|
1331
|
+
verificationUrl: `${getFrontendBaseUrl(options)}/verify-email?token=${token}`,
|
|
1332
|
+
expiresIn: EMAIL_VERIFICATION_EXPIRES_IN
|
|
1263
1333
|
}),
|
|
1264
1334
|
from: COMPANY_NAME
|
|
1265
1335
|
});
|
|
@@ -1285,7 +1355,7 @@ function createAuthRouter(options = {}) {
|
|
|
1285
1355
|
firstName: user.firstName,
|
|
1286
1356
|
lastName: user.lastName
|
|
1287
1357
|
},
|
|
1288
|
-
|
|
1358
|
+
PASSWORD_RESET_TTL_SEC
|
|
1289
1359
|
);
|
|
1290
1360
|
const resetResult = await sendRateLimitedEmail({
|
|
1291
1361
|
emailService: email,
|
|
@@ -1294,13 +1364,8 @@ function createAuthRouter(options = {}) {
|
|
|
1294
1364
|
// html: buildResetTemplate(resetToken, options),
|
|
1295
1365
|
html: buildResetPasswordEmailTemplate({
|
|
1296
1366
|
firstName: user.firstName,
|
|
1297
|
-
resetUrl: `${getFrontendBaseUrl(options)}/reset-password?token=${
|
|
1298
|
-
|
|
1299
|
-
userId: user.id,
|
|
1300
|
-
email: user.email
|
|
1301
|
-
}
|
|
1302
|
-
)}`,
|
|
1303
|
-
expiresIn: "1 hour"
|
|
1367
|
+
resetUrl: `${getFrontendBaseUrl(options)}/reset-password?token=${resetToken}`,
|
|
1368
|
+
expiresIn: PASSWORD_RESET_EXPIRES_IN
|
|
1304
1369
|
}),
|
|
1305
1370
|
from: COMPANY_NAME
|
|
1306
1371
|
});
|
|
@@ -1469,7 +1534,9 @@ function createAuthRouter(options = {}) {
|
|
|
1469
1534
|
}
|
|
1470
1535
|
const stateData = {
|
|
1471
1536
|
redirectTo: req.query.redirectTo || "",
|
|
1472
|
-
projectId: req.query.projectId || process.env.DEFAULT_PROJECT_ID || ""
|
|
1537
|
+
projectId: req.query.projectId || process.env.DEFAULT_PROJECT_ID || "",
|
|
1538
|
+
metadata: req.query.metadata || "",
|
|
1539
|
+
mode: req.query.mode || ""
|
|
1473
1540
|
};
|
|
1474
1541
|
const state = encodeURIComponent(JSON.stringify(stateData));
|
|
1475
1542
|
const params = new URLSearchParams({
|
|
@@ -1490,7 +1557,7 @@ function createAuthRouter(options = {}) {
|
|
|
1490
1557
|
return res.status(500).json({ error: "Google login not configured" });
|
|
1491
1558
|
}
|
|
1492
1559
|
const code = String(req.query.code || "");
|
|
1493
|
-
let stateData = { redirectTo: "", projectId: "" };
|
|
1560
|
+
let stateData = { redirectTo: "", projectId: "", metadata: "", mode: "" };
|
|
1494
1561
|
try {
|
|
1495
1562
|
if (req.query.state) {
|
|
1496
1563
|
stateData = JSON.parse(decodeURIComponent(String(req.query.state)));
|
|
@@ -1499,12 +1566,24 @@ function createAuthRouter(options = {}) {
|
|
|
1499
1566
|
console.error("Failed to parse state:", err);
|
|
1500
1567
|
}
|
|
1501
1568
|
const { redirectTo, projectId } = stateData;
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1569
|
+
const stateMetadataRaw = stateData?.metadata;
|
|
1570
|
+
const extraMetadata = [];
|
|
1571
|
+
if (stateMetadataRaw) {
|
|
1572
|
+
try {
|
|
1573
|
+
const parsed = typeof stateMetadataRaw === "string" ? JSON.parse(stateMetadataRaw) : stateMetadataRaw;
|
|
1574
|
+
if (Array.isArray(parsed)) {
|
|
1575
|
+
for (const item of parsed) {
|
|
1576
|
+
if (item?.key != null) extraMetadata.push(item);
|
|
1577
|
+
}
|
|
1578
|
+
} else if (parsed && typeof parsed === "object") {
|
|
1579
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1580
|
+
extraMetadata.push({ key, value });
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
console.warn("Failed to parse google state metadata:", e);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1508
1587
|
if (!code) {
|
|
1509
1588
|
return res.status(400).json({ ok: false, error: "Missing authorization code" });
|
|
1510
1589
|
}
|
|
@@ -1536,9 +1615,9 @@ function createAuthRouter(options = {}) {
|
|
|
1536
1615
|
}
|
|
1537
1616
|
const emailVerified = decoded.email_verified ?? true;
|
|
1538
1617
|
const firstName = decoded.given_name || "";
|
|
1539
|
-
const lastName = decoded.family_name || "";
|
|
1540
|
-
let
|
|
1541
|
-
if (!
|
|
1618
|
+
const lastName = decoded.family_name || firstName || (typeof email2 === "string" ? email2.split("@")[0] : "") || "User";
|
|
1619
|
+
let userDoc = await OrgUser.findOne({ email: email2 });
|
|
1620
|
+
if (!userDoc) {
|
|
1542
1621
|
const finalProjectId = projectId || process.env.DEFAULT_PROJECT_ID;
|
|
1543
1622
|
if (!finalProjectId) {
|
|
1544
1623
|
console.error("No projectId available for new user");
|
|
@@ -1552,11 +1631,46 @@ function createAuthRouter(options = {}) {
|
|
|
1552
1631
|
emailVerified,
|
|
1553
1632
|
roles: ["platform_user"],
|
|
1554
1633
|
projectId: finalProjectId,
|
|
1555
|
-
metadata: []
|
|
1556
|
-
|
|
1634
|
+
metadata: [...extraMetadata || []],
|
|
1635
|
+
providers: ["google"],
|
|
1636
|
+
googleId: decoded.sub
|
|
1557
1637
|
});
|
|
1558
|
-
|
|
1638
|
+
userDoc = created;
|
|
1639
|
+
} else {
|
|
1640
|
+
const providers = userDoc.providers;
|
|
1641
|
+
if (Array.isArray(providers) && providers.includes("password") && !providers.includes("google")) {
|
|
1642
|
+
const errorRedirect = (redirectTo || googleDefaultRedirect) + (redirectTo?.includes("?") ? "&" : "?") + "error=google_not_linked";
|
|
1643
|
+
return res.redirect(errorRedirect);
|
|
1644
|
+
}
|
|
1645
|
+
if (!providers || providers.length === 0) {
|
|
1646
|
+
const errorRedirect = (redirectTo || googleDefaultRedirect) + (redirectTo?.includes("?") ? "&" : "?") + "error=google_not_linked";
|
|
1647
|
+
return res.redirect(errorRedirect);
|
|
1648
|
+
}
|
|
1649
|
+
try {
|
|
1650
|
+
const current = new Map(
|
|
1651
|
+
(userDoc.metadata || []).map((m) => [
|
|
1652
|
+
m.key,
|
|
1653
|
+
m.value
|
|
1654
|
+
])
|
|
1655
|
+
);
|
|
1656
|
+
for (const item of extraMetadata || []) {
|
|
1657
|
+
if (item?.key != null) current.set(item.key, item.value);
|
|
1658
|
+
}
|
|
1659
|
+
userDoc.metadata = Array.from(current.entries()).map(
|
|
1660
|
+
([key, value]) => ({ key, value })
|
|
1661
|
+
);
|
|
1662
|
+
} catch (e) {
|
|
1663
|
+
console.warn("Failed to merge metadata onto existing user:", e);
|
|
1664
|
+
}
|
|
1665
|
+
if (!providers.includes("google")) {
|
|
1666
|
+
userDoc.providers = [...providers, "google"];
|
|
1667
|
+
}
|
|
1668
|
+
if (!userDoc.googleId) {
|
|
1669
|
+
userDoc.googleId = decoded.sub;
|
|
1670
|
+
}
|
|
1671
|
+
await userDoc.save();
|
|
1559
1672
|
}
|
|
1673
|
+
const user = userDoc.toObject();
|
|
1560
1674
|
const tokens = generateTokens(user);
|
|
1561
1675
|
setAuthCookies(res, tokens, cookieConfig);
|
|
1562
1676
|
if (user.projectId) {
|
|
@@ -1992,7 +2106,7 @@ function generateTokens(user) {
|
|
|
1992
2106
|
expiresIn: "1d"
|
|
1993
2107
|
});
|
|
1994
2108
|
const refreshToken = import_jsonwebtoken4.default.sign(
|
|
1995
|
-
{ sub: user.
|
|
2109
|
+
{ sub: user._id.toString() },
|
|
1996
2110
|
process.env.JWT_SECRET,
|
|
1997
2111
|
{ expiresIn: "30d" }
|
|
1998
2112
|
);
|