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 CHANGED
@@ -1,3 +1,3 @@
1
- MIT License
2
-
1
+ MIT License
2
+
3
3
  (See standard MIT text)
@@ -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 || !lastName)
422
- return res.status(400).json({ error: "firstName,lastName required" });
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 user = await OrgUser.findOne({ email: emailAddress }).select("+password").lean();
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: "1 hour"
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=${email.sign(
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
- 60 * 60
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=${email.sign(
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
- console.log(
1503
- "Parsed state - redirectTo:",
1504
- redirectTo,
1505
- "projectId:",
1506
- projectId
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 user = await OrgUser.findOne({ email: email2 }).lean();
1541
- if (!user) {
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
- // you can also store googleId: decoded.sub
1634
+ metadata: [...extraMetadata || []],
1635
+ providers: ["google"],
1636
+ googleId: decoded.sub
1557
1637
  });
1558
- user = created.toObject();
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.id.toString() },
2109
+ { sub: user._id.toString() },
1996
2110
  process.env.JWT_SECRET,
1997
2111
  { expiresIn: "30d" }
1998
2112
  );