aaspai-authx 0.1.4 → 0.1.6

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 CHANGED
@@ -100,8 +100,8 @@ function loadConfig() {
100
100
  cookies: {
101
101
  domain: process.env.COOKIE_DOMAIN,
102
102
  secure: (process.env.COOKIE_SECURE || "true") === "true",
103
- accessTtlMs: 24 * 60 * 60 * 1e3,
104
- refreshTtlMs: 7 * 24 * 60 * 60 * 1e3
103
+ accessTtlMs: 7 * 24 * 60 * 60 * 1e3,
104
+ refreshTtlMs: 30 * 24 * 60 * 60 * 1e3
105
105
  },
106
106
  oidc: {
107
107
  jwtSecret: process.env.JWT_SECRET
@@ -642,7 +642,7 @@ var AuthAdminService = class {
642
642
  }
643
643
  async updateUserPassword(userId, newPassword) {
644
644
  const hashed = await import_bcrypt.default.hash(newPassword, 10);
645
- await OrgUser.findOneAndUpdate({ id: userId }, { password: hashed });
645
+ await OrgUser.findOneAndUpdate({ id: userId }, { passwordHash: hashed });
646
646
  }
647
647
  // -------------------------------------------------------------------
648
648
  // ADMIN TOKEN (self-issued JWT)
@@ -657,11 +657,11 @@ var AuthAdminService = class {
657
657
  system: true
658
658
  };
659
659
  const accessToken = import_jsonwebtoken2.default.sign(payload, process.env.JWT_SECRET, {
660
- expiresIn: "1h"
660
+ expiresIn: "1d"
661
661
  });
662
662
  this.token = {
663
663
  accessToken,
664
- exp: now + 3600
664
+ exp: now + 84800
665
665
  };
666
666
  return this.token.accessToken;
667
667
  }
@@ -686,7 +686,7 @@ var EmailService = class {
686
686
  }
687
687
  });
688
688
  }
689
- sign(payload, ttlSec = 60 * 60 * 24) {
689
+ sign(payload, ttlSec = 60 * 60 * 24 * 30) {
690
690
  return import_jsonwebtoken3.default.sign(payload, process.env.EMAIL_JWT_SECRET, {
691
691
  expiresIn: ttlSec
692
692
  });
@@ -694,11 +694,10 @@ var EmailService = class {
694
694
  verify(token) {
695
695
  return import_jsonwebtoken3.default.verify(token, process.env.EMAIL_JWT_SECRET);
696
696
  }
697
- async send(to, subject, html) {
698
- console.log("[EmailService] Attempting to send:", { to, subject });
697
+ async send(to, subject, html, from) {
699
698
  try {
700
699
  const info = await this.transporter.sendMail({
701
- from: process.env.EMAIL_FROM,
700
+ from: from ? `${from} ` + process.env.EMAIL_FROM : process.env.EMAIL_FROM,
702
701
  to,
703
702
  subject,
704
703
  html
@@ -723,18 +722,6 @@ var EmailService = class {
723
722
  }
724
723
  }
725
724
  canSend(lastEmailSent) {
726
- console.log(
727
- process.env.EMAIL_PASSWORD,
728
- "pssword",
729
- process.env.EMAIL_USER,
730
- "user",
731
- process.env.EMAIL_SECURE,
732
- "secure",
733
- process.env.EMAIL_PORT,
734
- "porat",
735
- process.env.EMAIL_HOST,
736
- "hosat"
737
- );
738
725
  const now = Date.now();
739
726
  const windowStart = now - this.WINDOW_MINUTES * 60 * 1e3;
740
727
  const emailsInWindow = (lastEmailSent || []).map((d) => new Date(d)).filter((d) => d.getTime() >= windowStart);
@@ -748,6 +735,386 @@ var EmailService = class {
748
735
  }
749
736
  };
750
737
 
738
+ // src/templates/email.templates.ts
739
+ var colors = {
740
+ background: "#0a0a0a",
741
+ cardBackground: "#111111",
742
+ cardBorder: "#1a1a1a",
743
+ accent: "#ffffff",
744
+ accentMuted: "rgba(255, 255, 255, 0.9)",
745
+ textPrimary: "#ffffff",
746
+ textSecondary: "rgba(255, 255, 255, 0.7)",
747
+ textMuted: "rgba(255, 255, 255, 0.5)",
748
+ divider: "rgba(255, 255, 255, 0.1)",
749
+ subtle: "#161616",
750
+ highlight: "rgba(255, 255, 255, 0.05)"
751
+ };
752
+ var styles = {
753
+ wrapper: `
754
+ margin: 0;
755
+ padding: 40px 20px;
756
+ background-color: ${colors.background};
757
+ background-image:
758
+ radial-gradient(ellipse at top, rgba(255,255,255,0.03) 0%, transparent 50%),
759
+ radial-gradient(ellipse at bottom, rgba(255,255,255,0.02) 0%, transparent 50%);
760
+ min-height: 100vh;
761
+ `,
762
+ container: `
763
+ max-width: 520px;
764
+ margin: 0 auto;
765
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
766
+ color: ${colors.textPrimary};
767
+ line-height: 1.7;
768
+ `,
769
+ card: `
770
+ background-color: ${colors.cardBackground};
771
+ border: 1px solid ${colors.cardBorder};
772
+ border-radius: 16px;
773
+ overflow: hidden;
774
+ box-shadow:
775
+ 0 0 0 1px rgba(255,255,255,0.05),
776
+ 0 20px 50px -20px rgba(0,0,0,0.5),
777
+ 0 30px 60px -30px rgba(0,0,0,0.3);
778
+ `,
779
+ header: `
780
+ padding: 48px 40px 32px;
781
+ text-align: center;
782
+ border-bottom: 1px solid ${colors.divider};
783
+ `,
784
+ iconWrapper: `
785
+ width: 64px;
786
+ height: 64px;
787
+ margin: 0 auto 24px;
788
+ background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
789
+ border: 1px solid rgba(255,255,255,0.1);
790
+ border-radius: 16px;
791
+ display: flex;
792
+ align-items: center;
793
+ justify-content: center;
794
+ font-size: 28px;
795
+ `,
796
+ headerTitle: `
797
+ color: ${colors.textPrimary};
798
+ margin: 0;
799
+ font-size: 24px;
800
+ font-weight: 600;
801
+ letter-spacing: -0.5px;
802
+ `,
803
+ headerSubtitle: `
804
+ color: ${colors.textMuted};
805
+ margin: 8px 0 0;
806
+ font-size: 14px;
807
+ font-weight: 400;
808
+ `,
809
+ body: `
810
+ padding: 40px;
811
+ `,
812
+ greeting: `
813
+ margin: 0 0 24px;
814
+ color: ${colors.textPrimary};
815
+ font-size: 18px;
816
+ font-weight: 500;
817
+ `,
818
+ paragraph: `
819
+ margin: 0 0 20px;
820
+ color: ${colors.textSecondary};
821
+ font-size: 15px;
822
+ line-height: 1.7;
823
+ `,
824
+ buttonWrapper: `
825
+ text-align: center;
826
+ margin: 32px 0;
827
+ `,
828
+ button: `
829
+ display: inline-block;
830
+ background-color: ${colors.accent};
831
+ color: #000000 !important;
832
+ text-decoration: none;
833
+ padding: 14px 36px;
834
+ border-radius: 8px;
835
+ font-weight: 600;
836
+ font-size: 14px;
837
+ letter-spacing: 0.3px;
838
+ transition: all 0.2s ease;
839
+ `,
840
+ secondaryButton: `
841
+ display: inline-block;
842
+ background-color: transparent;
843
+ color: ${colors.textPrimary} !important;
844
+ text-decoration: none;
845
+ padding: 12px 28px;
846
+ border-radius: 8px;
847
+ font-weight: 500;
848
+ font-size: 14px;
849
+ border: 1px solid ${colors.divider};
850
+ `,
851
+ infoCard: `
852
+ background-color: ${colors.subtle};
853
+ border: 1px solid ${colors.divider};
854
+ border-radius: 12px;
855
+ padding: 20px 24px;
856
+ margin: 28px 0;
857
+ `,
858
+ infoCardTitle: `
859
+ margin: 0 0 12px;
860
+ color: ${colors.textPrimary};
861
+ font-size: 13px;
862
+ font-weight: 600;
863
+ text-transform: uppercase;
864
+ letter-spacing: 0.5px;
865
+ `,
866
+ infoCardText: `
867
+ margin: 0;
868
+ color: ${colors.textSecondary};
869
+ font-size: 14px;
870
+ line-height: 1.6;
871
+ `,
872
+ warningCard: `
873
+ background: linear-gradient(135deg, rgba(255,180,0,0.1) 0%, rgba(255,140,0,0.05) 100%);
874
+ border: 1px solid rgba(255,180,0,0.2);
875
+ border-radius: 12px;
876
+ padding: 20px 24px;
877
+ margin: 28px 0;
878
+ `,
879
+ warningCardTitle: `
880
+ margin: 0 0 12px;
881
+ color: #ffc107;
882
+ font-size: 13px;
883
+ font-weight: 600;
884
+ text-transform: uppercase;
885
+ letter-spacing: 0.5px;
886
+ `,
887
+ warningCardText: `
888
+ margin: 0;
889
+ color: rgba(255,255,255,0.7);
890
+ font-size: 14px;
891
+ line-height: 1.6;
892
+ `,
893
+ successCard: `
894
+ background: linear-gradient(135deg, rgba(0,255,150,0.1) 0%, rgba(0,200,100,0.05) 100%);
895
+ border: 1px solid rgba(0,255,150,0.2);
896
+ border-radius: 12px;
897
+ padding: 20px 24px;
898
+ margin: 28px 0;
899
+ `,
900
+ successCardTitle: `
901
+ margin: 0 0 12px;
902
+ color: #00ff96;
903
+ font-size: 13px;
904
+ font-weight: 600;
905
+ text-transform: uppercase;
906
+ letter-spacing: 0.5px;
907
+ `,
908
+ linkSection: `
909
+ margin: 32px 0;
910
+ padding: 20px;
911
+ background-color: ${colors.subtle};
912
+ border-radius: 8px;
913
+ border: 1px solid ${colors.divider};
914
+ `,
915
+ linkLabel: `
916
+ margin: 0 0 8px;
917
+ color: ${colors.textMuted};
918
+ font-size: 12px;
919
+ text-transform: uppercase;
920
+ letter-spacing: 0.5px;
921
+ `,
922
+ linkText: `
923
+ word-break: break-all;
924
+ color: ${colors.textSecondary};
925
+ font-size: 13px;
926
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
927
+ margin: 0;
928
+ `,
929
+ divider: `
930
+ border: none;
931
+ border-top: 1px solid ${colors.divider};
932
+ margin: 32px 0;
933
+ `,
934
+ footer: `
935
+ padding: 24px 40px 32px;
936
+ text-align: center;
937
+ border-top: 1px solid ${colors.divider};
938
+ `,
939
+ footerText: `
940
+ margin: 0;
941
+ color: ${colors.textMuted};
942
+ font-size: 12px;
943
+ line-height: 1.8;
944
+ `,
945
+ footerLink: `
946
+ color: ${colors.textSecondary};
947
+ text-decoration: none;
948
+ `,
949
+ badge: `
950
+ display: inline-block;
951
+ background-color: rgba(255,255,255,0.1);
952
+ color: ${colors.textSecondary};
953
+ padding: 4px 12px;
954
+ border-radius: 20px;
955
+ font-size: 12px;
956
+ font-weight: 500;
957
+ letter-spacing: 0.3px;
958
+ `,
959
+ listItem: `
960
+ color: ${colors.textSecondary};
961
+ font-size: 14px;
962
+ margin: 8px 0;
963
+ padding-left: 8px;
964
+ `,
965
+ metaRow: `
966
+ display: flex;
967
+ justify-content: space-between;
968
+ padding: 12px 0;
969
+ border-bottom: 1px solid ${colors.divider};
970
+ `,
971
+ metaLabel: `
972
+ color: ${colors.textMuted};
973
+ font-size: 13px;
974
+ `,
975
+ metaValue: `
976
+ color: ${colors.textPrimary};
977
+ font-size: 13px;
978
+ font-weight: 500;
979
+ `
980
+ };
981
+ function buildVerificationEmailTemplate(data) {
982
+ const { firstName, verificationUrl, expiresIn } = data;
983
+ return `
984
+ <!DOCTYPE html>
985
+ <html lang="en">
986
+ <head>
987
+ <meta charset="UTF-8">
988
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
989
+ <meta name="color-scheme" content="dark">
990
+ <meta name="supported-color-schemes" content="dark">
991
+ <title>Verify Your Email</title>
992
+ <!--[if mso]>
993
+ <style type="text/css">
994
+ body, table, td {font-family: Arial, Helvetica, sans-serif !important;}
995
+ </style>
996
+ <![endif]-->
997
+ </head>
998
+ <body style="${styles.wrapper}">
999
+ <div style="${styles.container}">
1000
+ <div style="${styles.card}">
1001
+ <!-- Header -->
1002
+ <div style="${styles.header}">
1003
+ <div style="${styles.iconWrapper}">
1004
+ \u2709\uFE0F
1005
+ </div>
1006
+ <h1 style="${styles.headerTitle}">Verify your email</h1>
1007
+ <p style="${styles.headerSubtitle}">One quick step to get started</p>
1008
+ </div>
1009
+
1010
+ <!-- Body -->
1011
+ <div style="${styles.body}">
1012
+ <p style="${styles.greeting}">Hi ${firstName},</p>
1013
+
1014
+ <p style="${styles.paragraph}">
1015
+ Thanks for signing up. To complete your registration and unlock all features,
1016
+ please verify your email address by clicking the button below.
1017
+ </p>
1018
+
1019
+ <div style="${styles.buttonWrapper}">
1020
+ <a href="${verificationUrl}" style="${styles.button}" target="_blank">
1021
+ Verify Email Address
1022
+ </a>
1023
+ </div>
1024
+
1025
+ <div style="${styles.infoCard}">
1026
+ <p style="${styles.infoCardTitle}">\u23F1 Time Sensitive</p>
1027
+ <p style="${styles.infoCardText}">
1028
+ This verification link will expire in <strong>${expiresIn}</strong>.
1029
+ If you didn't create an account, you can safely ignore this email.
1030
+ </p>
1031
+ </div>
1032
+ </div>
1033
+
1034
+ <!-- Footer -->
1035
+ <div style="${styles.footer}">
1036
+ <p style="${styles.footerText}">
1037
+ This is an automated message \u2014 please do not reply.<br>
1038
+ \xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} All rights reserved.
1039
+ </p>
1040
+ </div>
1041
+ </div>
1042
+ </div>
1043
+ </body>
1044
+ </html>
1045
+ `;
1046
+ }
1047
+ function buildResetPasswordEmailTemplate(data) {
1048
+ const { firstName, resetUrl, expiresIn } = data;
1049
+ return `
1050
+ <!DOCTYPE html>
1051
+ <html lang="en">
1052
+ <head>
1053
+ <meta charset="UTF-8">
1054
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1055
+ <meta name="color-scheme" content="dark">
1056
+ <meta name="supported-color-schemes" content="dark">
1057
+ <title>Reset Your Password</title>
1058
+ </head>
1059
+ <body style="${styles.wrapper}">
1060
+ <div style="${styles.container}">
1061
+ <div style="${styles.card}">
1062
+ <!-- Header -->
1063
+ <div style="${styles.header}">
1064
+ <div style="${styles.iconWrapper}">
1065
+ \u{1F510}
1066
+ </div>
1067
+ <h1 style="${styles.headerTitle}">Reset your password</h1>
1068
+ <p style="${styles.headerSubtitle}">We received a reset request</p>
1069
+ </div>
1070
+
1071
+ <!-- Body -->
1072
+ <div style="${styles.body}">
1073
+ <p style="${styles.greeting}">Hi ${firstName},</p>
1074
+
1075
+ <p style="${styles.paragraph}">
1076
+ We received a request to reset the password for your account.
1077
+ Click the button below to create a new password.
1078
+ </p>
1079
+
1080
+ <div style="${styles.buttonWrapper}">
1081
+ <a href="${resetUrl}" style="${styles.button}" target="_blank">
1082
+ Reset Password
1083
+ </a>
1084
+ </div>
1085
+
1086
+ <div style="${styles.warningCard}">
1087
+ <p style="${styles.warningCardTitle}">\u26A0\uFE0F Security Notice</p>
1088
+ <p style="${styles.warningCardText}">
1089
+ \u2022 This link expires in <strong>${expiresIn}</strong><br>
1090
+ \u2022 This link can only be used once<br>
1091
+ \u2022 If you didn't request this, ignore this email
1092
+ </p>
1093
+ </div>
1094
+
1095
+ <hr style="${styles.divider}" />
1096
+
1097
+ <p style="${styles.paragraph}; font-size: 13px; color: ${colors.textMuted};">
1098
+ <strong>Didn't request this?</strong><br>
1099
+ Your password remains unchanged. If you're concerned about your account
1100
+ security, please contact our support team immediately.
1101
+ </p>
1102
+ </div>
1103
+
1104
+ <!-- Footer -->
1105
+ <div style="${styles.footer}">
1106
+ <p style="${styles.footerText}">
1107
+ This is an automated message \u2014 please do not reply.<br>
1108
+ \xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} All rights reserved.
1109
+ </p>
1110
+ </div>
1111
+ </div>
1112
+ </div>
1113
+ </body>
1114
+ </html>
1115
+ `;
1116
+ }
1117
+
751
1118
  // src/express/auth.routes.ts
752
1119
  function createAuthRouter(options = {}) {
753
1120
  const googleClientId = process.env.GOOGLE_CLIENT_ID;
@@ -769,7 +1136,7 @@ function createAuthRouter(options = {}) {
769
1136
  // default: secure in prod
770
1137
  domain: options.cookie?.domain ?? void 0,
771
1138
  path: options.cookie?.path ?? "/",
772
- maxAgeMs: options.cookie?.maxAgeMs ?? 24 * 60 * 60 * 1e3
1139
+ maxAgeMs: options.cookie?.maxAgeMs ?? 30 * 24 * 60 * 60 * 1e3
773
1140
  };
774
1141
  r.use(import_express.default.json());
775
1142
  r.use(import_express.default.urlencoded({ extended: true }));
@@ -826,6 +1193,7 @@ function createAuthRouter(options = {}) {
826
1193
  projectId,
827
1194
  metadata
828
1195
  } = req.body || {};
1196
+ const COMPANY_NAME = process.env.COMPANY_NAME;
829
1197
  try {
830
1198
  const kcUser = await authAdmin.createUserInRealm({
831
1199
  username: emailAddress,
@@ -854,10 +1222,21 @@ function createAuthRouter(options = {}) {
854
1222
  emailService: email,
855
1223
  user,
856
1224
  subject: "Verify your email",
857
- html: buildVerificationTemplate(
858
- email.sign({ userId: kcUser.id, email: kcUser.email }),
859
- options
860
- )
1225
+ // html: buildVerificationTemplate(
1226
+ // email.sign({ userId: kcUser.id, email: kcUser.email }),
1227
+ // options,
1228
+ // ),
1229
+ html: buildVerificationEmailTemplate({
1230
+ firstName: user.firstName,
1231
+ verificationUrl: `${getFrontendBaseUrl(options)}/verify-email?token=${email.sign(
1232
+ {
1233
+ userId: user.id,
1234
+ email: user.email
1235
+ }
1236
+ )}`,
1237
+ expiresIn: "1 hour"
1238
+ }),
1239
+ from: COMPANY_NAME
861
1240
  });
862
1241
  if (emailResult.rateLimited) {
863
1242
  return res.status(429).json({
@@ -922,6 +1301,7 @@ function createAuthRouter(options = {}) {
922
1301
  "/resend-verification-email",
923
1302
  validateResendEmail,
924
1303
  async (req, res) => {
1304
+ const COMPANY_NAME = process.env.COMPANY_NAME;
925
1305
  const user = await OrgUser.findOne({ email: req.body.email });
926
1306
  if (!user)
927
1307
  return res.status(404).json({ ok: false, error: "User not found" });
@@ -937,7 +1317,18 @@ function createAuthRouter(options = {}) {
937
1317
  emailService: email,
938
1318
  user,
939
1319
  subject: "Verify your email",
940
- html: buildVerificationTemplate(token, options)
1320
+ // html: buildVerificationTemplate(token, options),
1321
+ html: buildVerificationEmailTemplate({
1322
+ firstName: user.firstName,
1323
+ verificationUrl: `${getFrontendBaseUrl(options)}/verify-email?token=${email.sign(
1324
+ {
1325
+ userId: user.id,
1326
+ email: user.email
1327
+ }
1328
+ )}`,
1329
+ expiresIn: "1 hour"
1330
+ }),
1331
+ from: COMPANY_NAME
941
1332
  });
942
1333
  if (resendResult.rateLimited) {
943
1334
  return res.status(429).json({
@@ -950,6 +1341,7 @@ function createAuthRouter(options = {}) {
950
1341
  }
951
1342
  );
952
1343
  r.post("/forgot-password", validateResendEmail, async (req, res) => {
1344
+ const COMPANY_NAME = process.env.COMPANY_NAME;
953
1345
  const user = await OrgUser.findOne({ email: req.body.email });
954
1346
  if (!user)
955
1347
  return res.status(404).json({ ok: false, error: "User not found" });
@@ -966,7 +1358,18 @@ function createAuthRouter(options = {}) {
966
1358
  emailService: email,
967
1359
  user,
968
1360
  subject: "Reset password",
969
- html: buildResetTemplate(resetToken, options)
1361
+ // html: buildResetTemplate(resetToken, options),
1362
+ html: buildResetPasswordEmailTemplate({
1363
+ firstName: user.firstName,
1364
+ resetUrl: `${getFrontendBaseUrl(options)}/reset-password?token=${email.sign(
1365
+ {
1366
+ userId: user.id,
1367
+ email: user.email
1368
+ }
1369
+ )}`,
1370
+ expiresIn: "1 hour"
1371
+ }),
1372
+ from: COMPANY_NAME
970
1373
  });
971
1374
  if (resetResult.rateLimited) {
972
1375
  return res.status(429).json({
@@ -979,9 +1382,16 @@ function createAuthRouter(options = {}) {
979
1382
  });
980
1383
  r.post("/reset-password", validateResetPassword, async (req, res) => {
981
1384
  const { token, newPassword } = req.body || {};
1385
+ if (!token || !newPassword) {
1386
+ return res.status(400).json({
1387
+ ok: false,
1388
+ error: "Token and new password are required",
1389
+ code: "MISSING_FIELDS"
1390
+ });
1391
+ }
982
1392
  try {
983
1393
  const payload = email.verify(token);
984
- const user = await OrgUser.findOne({ keycloakId: payload.userId });
1394
+ const user = await OrgUser.findOne({ id: payload.userId });
985
1395
  if (!user) {
986
1396
  return res.status(404).json({ ok: false, error: "User not found" });
987
1397
  }
@@ -1349,8 +1759,6 @@ function setAuthCookies(res, tokens, cookie) {
1349
1759
  if (cookie.domain) {
1350
1760
  base.domain = cookie.domain;
1351
1761
  }
1352
- console.log(cookie, "cookie");
1353
- console.log(base, "base");
1354
1762
  if (tokens?.access_token) {
1355
1763
  res.cookie("access_token", tokens.access_token, base);
1356
1764
  }
@@ -1374,12 +1782,6 @@ function respondWithKeycloakError(res, err, fallback, status = 400) {
1374
1782
  const description = err?.response?.data?.error_description || err?.response?.data?.errorMessage || err?.message || fallback;
1375
1783
  return res.status(status).json({ ok: false, error: description });
1376
1784
  }
1377
- function buildVerificationTemplate(token, options) {
1378
- return `<a href="${getFrontendBaseUrl(options)}/auth/verify-email?token=${token}">Verify</a>`;
1379
- }
1380
- function buildResetTemplate(token, options) {
1381
- return `<a href="${getFrontendBaseUrl(options)}/auth/reset-password?token=${token}">Reset</a>`;
1382
- }
1383
1785
  function getFrontendBaseUrl(options) {
1384
1786
  if (options.frontendBaseUrl)
1385
1787
  return options.frontendBaseUrl.replace(/\/$/, "");
@@ -1391,13 +1793,14 @@ async function sendRateLimitedEmail({
1391
1793
  emailService,
1392
1794
  user,
1393
1795
  subject,
1394
- html
1796
+ html,
1797
+ from
1395
1798
  }) {
1396
1799
  const can = emailService.canSend(user?.lastEmailSent || []);
1397
1800
  if (!can.ok) {
1398
1801
  return { rateLimited: true, waitMs: can.waitMs };
1399
1802
  }
1400
- await emailService.send(user.email, subject, html);
1803
+ await emailService.send(user.email, subject, html, from);
1401
1804
  user.lastEmailSent = [...user.lastEmailSent || [], /* @__PURE__ */ new Date()];
1402
1805
  await user.save();
1403
1806
  return { rateLimited: false };
@@ -1418,7 +1821,7 @@ function generateTokens(user) {
1418
1821
  type: "user"
1419
1822
  };
1420
1823
  const accessToken = import_jsonwebtoken4.default.sign(accessPayload, process.env.JWT_SECRET, {
1421
- expiresIn: "1h"
1824
+ expiresIn: "1d"
1422
1825
  });
1423
1826
  const refreshToken = import_jsonwebtoken4.default.sign(
1424
1827
  { sub: user._id.toString() },
@@ -1454,13 +1857,61 @@ function createDashboardRouter(options) {
1454
1857
  }
1455
1858
 
1456
1859
  // src/express/email.routes.ts
1457
- var import_express3 = require("express");
1860
+ var import_express3 = __toESM(require("express"), 1);
1458
1861
  function createEmailRouter(options) {
1459
1862
  const r = (0, import_express3.Router)();
1863
+ const emailService = new EmailService();
1864
+ r.use(import_express3.default.json());
1865
+ r.use(import_express3.default.urlencoded({ extended: true }));
1460
1866
  r.get(
1461
1867
  "/verify",
1462
1868
  (req, res) => res.json({ ok: true, token: req.query.token })
1463
1869
  );
1870
+ r.post("/send", async (req, res) => {
1871
+ try {
1872
+ const { userId, to, subject, html, from } = req.body ?? {};
1873
+ if (!to || !subject || !html) {
1874
+ return res.status(400).json({
1875
+ ok: false,
1876
+ error: "BAD_REQUEST",
1877
+ message: "`to`, `subject`, and `html` are required."
1878
+ });
1879
+ }
1880
+ if (userId) {
1881
+ const user = await OrgUser.findOne({ id: userId }).lean();
1882
+ if (!user) {
1883
+ return res.status(404).json({
1884
+ ok: false,
1885
+ error: "NOT_FOUND",
1886
+ message: "User not found."
1887
+ });
1888
+ }
1889
+ const can = emailService.canSend(user?.lastEmailSent || []);
1890
+ if (!can.ok) {
1891
+ return res.status(429).json({
1892
+ ok: false,
1893
+ error: can.reason,
1894
+ waitMs: can.waitMs,
1895
+ message: "Too many emails sent recently. Please retry later."
1896
+ });
1897
+ }
1898
+ }
1899
+ await emailService.send(to, subject, html, from);
1900
+ if (userId) {
1901
+ await OrgUser.updateOne(
1902
+ { id: userId },
1903
+ { $push: { lastEmailSent: /* @__PURE__ */ new Date() } }
1904
+ );
1905
+ }
1906
+ return res.json({ ok: true });
1907
+ } catch (err) {
1908
+ return res.status(500).json({
1909
+ ok: false,
1910
+ error: "INTERNAL",
1911
+ message: err?.message ?? "Error"
1912
+ });
1913
+ }
1914
+ });
1464
1915
  return r;
1465
1916
  }
1466
1917