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.
@@ -57,8 +57,8 @@ function loadConfig() {
57
57
  cookies: {
58
58
  domain: process.env.COOKIE_DOMAIN,
59
59
  secure: (process.env.COOKIE_SECURE || "true") === "true",
60
- accessTtlMs: 24 * 60 * 60 * 1e3,
61
- refreshTtlMs: 7 * 24 * 60 * 60 * 1e3
60
+ accessTtlMs: 7 * 24 * 60 * 60 * 1e3,
61
+ refreshTtlMs: 30 * 24 * 60 * 60 * 1e3
62
62
  },
63
63
  oidc: {
64
64
  jwtSecret: process.env.JWT_SECRET
@@ -558,7 +558,7 @@ var AuthAdminService = class {
558
558
  }
559
559
  async updateUserPassword(userId, newPassword) {
560
560
  const hashed = await import_bcrypt.default.hash(newPassword, 10);
561
- await OrgUser.findOneAndUpdate({ id: userId }, { password: hashed });
561
+ await OrgUser.findOneAndUpdate({ id: userId }, { passwordHash: hashed });
562
562
  }
563
563
  // -------------------------------------------------------------------
564
564
  // ADMIN TOKEN (self-issued JWT)
@@ -573,11 +573,11 @@ var AuthAdminService = class {
573
573
  system: true
574
574
  };
575
575
  const accessToken = import_jsonwebtoken2.default.sign(payload, process.env.JWT_SECRET, {
576
- expiresIn: "1h"
576
+ expiresIn: "1d"
577
577
  });
578
578
  this.token = {
579
579
  accessToken,
580
- exp: now + 3600
580
+ exp: now + 84800
581
581
  };
582
582
  return this.token.accessToken;
583
583
  }
@@ -602,7 +602,7 @@ var EmailService = class {
602
602
  }
603
603
  });
604
604
  }
605
- sign(payload, ttlSec = 60 * 60 * 24) {
605
+ sign(payload, ttlSec = 60 * 60 * 24 * 30) {
606
606
  return import_jsonwebtoken3.default.sign(payload, process.env.EMAIL_JWT_SECRET, {
607
607
  expiresIn: ttlSec
608
608
  });
@@ -610,11 +610,10 @@ var EmailService = class {
610
610
  verify(token) {
611
611
  return import_jsonwebtoken3.default.verify(token, process.env.EMAIL_JWT_SECRET);
612
612
  }
613
- async send(to, subject, html) {
614
- console.log("[EmailService] Attempting to send:", { to, subject });
613
+ async send(to, subject, html, from) {
615
614
  try {
616
615
  const info = await this.transporter.sendMail({
617
- from: process.env.EMAIL_FROM,
616
+ from: from ? `${from} ` + process.env.EMAIL_FROM : process.env.EMAIL_FROM,
618
617
  to,
619
618
  subject,
620
619
  html
@@ -639,18 +638,6 @@ var EmailService = class {
639
638
  }
640
639
  }
641
640
  canSend(lastEmailSent) {
642
- console.log(
643
- process.env.EMAIL_PASSWORD,
644
- "pssword",
645
- process.env.EMAIL_USER,
646
- "user",
647
- process.env.EMAIL_SECURE,
648
- "secure",
649
- process.env.EMAIL_PORT,
650
- "porat",
651
- process.env.EMAIL_HOST,
652
- "hosat"
653
- );
654
641
  const now = Date.now();
655
642
  const windowStart = now - this.WINDOW_MINUTES * 60 * 1e3;
656
643
  const emailsInWindow = (lastEmailSent || []).map((d) => new Date(d)).filter((d) => d.getTime() >= windowStart);
@@ -664,6 +651,386 @@ var EmailService = class {
664
651
  }
665
652
  };
666
653
 
654
+ // src/templates/email.templates.ts
655
+ var colors = {
656
+ background: "#0a0a0a",
657
+ cardBackground: "#111111",
658
+ cardBorder: "#1a1a1a",
659
+ accent: "#ffffff",
660
+ accentMuted: "rgba(255, 255, 255, 0.9)",
661
+ textPrimary: "#ffffff",
662
+ textSecondary: "rgba(255, 255, 255, 0.7)",
663
+ textMuted: "rgba(255, 255, 255, 0.5)",
664
+ divider: "rgba(255, 255, 255, 0.1)",
665
+ subtle: "#161616",
666
+ highlight: "rgba(255, 255, 255, 0.05)"
667
+ };
668
+ var styles = {
669
+ wrapper: `
670
+ margin: 0;
671
+ padding: 40px 20px;
672
+ background-color: ${colors.background};
673
+ background-image:
674
+ radial-gradient(ellipse at top, rgba(255,255,255,0.03) 0%, transparent 50%),
675
+ radial-gradient(ellipse at bottom, rgba(255,255,255,0.02) 0%, transparent 50%);
676
+ min-height: 100vh;
677
+ `,
678
+ container: `
679
+ max-width: 520px;
680
+ margin: 0 auto;
681
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
682
+ color: ${colors.textPrimary};
683
+ line-height: 1.7;
684
+ `,
685
+ card: `
686
+ background-color: ${colors.cardBackground};
687
+ border: 1px solid ${colors.cardBorder};
688
+ border-radius: 16px;
689
+ overflow: hidden;
690
+ box-shadow:
691
+ 0 0 0 1px rgba(255,255,255,0.05),
692
+ 0 20px 50px -20px rgba(0,0,0,0.5),
693
+ 0 30px 60px -30px rgba(0,0,0,0.3);
694
+ `,
695
+ header: `
696
+ padding: 48px 40px 32px;
697
+ text-align: center;
698
+ border-bottom: 1px solid ${colors.divider};
699
+ `,
700
+ iconWrapper: `
701
+ width: 64px;
702
+ height: 64px;
703
+ margin: 0 auto 24px;
704
+ background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
705
+ border: 1px solid rgba(255,255,255,0.1);
706
+ border-radius: 16px;
707
+ display: flex;
708
+ align-items: center;
709
+ justify-content: center;
710
+ font-size: 28px;
711
+ `,
712
+ headerTitle: `
713
+ color: ${colors.textPrimary};
714
+ margin: 0;
715
+ font-size: 24px;
716
+ font-weight: 600;
717
+ letter-spacing: -0.5px;
718
+ `,
719
+ headerSubtitle: `
720
+ color: ${colors.textMuted};
721
+ margin: 8px 0 0;
722
+ font-size: 14px;
723
+ font-weight: 400;
724
+ `,
725
+ body: `
726
+ padding: 40px;
727
+ `,
728
+ greeting: `
729
+ margin: 0 0 24px;
730
+ color: ${colors.textPrimary};
731
+ font-size: 18px;
732
+ font-weight: 500;
733
+ `,
734
+ paragraph: `
735
+ margin: 0 0 20px;
736
+ color: ${colors.textSecondary};
737
+ font-size: 15px;
738
+ line-height: 1.7;
739
+ `,
740
+ buttonWrapper: `
741
+ text-align: center;
742
+ margin: 32px 0;
743
+ `,
744
+ button: `
745
+ display: inline-block;
746
+ background-color: ${colors.accent};
747
+ color: #000000 !important;
748
+ text-decoration: none;
749
+ padding: 14px 36px;
750
+ border-radius: 8px;
751
+ font-weight: 600;
752
+ font-size: 14px;
753
+ letter-spacing: 0.3px;
754
+ transition: all 0.2s ease;
755
+ `,
756
+ secondaryButton: `
757
+ display: inline-block;
758
+ background-color: transparent;
759
+ color: ${colors.textPrimary} !important;
760
+ text-decoration: none;
761
+ padding: 12px 28px;
762
+ border-radius: 8px;
763
+ font-weight: 500;
764
+ font-size: 14px;
765
+ border: 1px solid ${colors.divider};
766
+ `,
767
+ infoCard: `
768
+ background-color: ${colors.subtle};
769
+ border: 1px solid ${colors.divider};
770
+ border-radius: 12px;
771
+ padding: 20px 24px;
772
+ margin: 28px 0;
773
+ `,
774
+ infoCardTitle: `
775
+ margin: 0 0 12px;
776
+ color: ${colors.textPrimary};
777
+ font-size: 13px;
778
+ font-weight: 600;
779
+ text-transform: uppercase;
780
+ letter-spacing: 0.5px;
781
+ `,
782
+ infoCardText: `
783
+ margin: 0;
784
+ color: ${colors.textSecondary};
785
+ font-size: 14px;
786
+ line-height: 1.6;
787
+ `,
788
+ warningCard: `
789
+ background: linear-gradient(135deg, rgba(255,180,0,0.1) 0%, rgba(255,140,0,0.05) 100%);
790
+ border: 1px solid rgba(255,180,0,0.2);
791
+ border-radius: 12px;
792
+ padding: 20px 24px;
793
+ margin: 28px 0;
794
+ `,
795
+ warningCardTitle: `
796
+ margin: 0 0 12px;
797
+ color: #ffc107;
798
+ font-size: 13px;
799
+ font-weight: 600;
800
+ text-transform: uppercase;
801
+ letter-spacing: 0.5px;
802
+ `,
803
+ warningCardText: `
804
+ margin: 0;
805
+ color: rgba(255,255,255,0.7);
806
+ font-size: 14px;
807
+ line-height: 1.6;
808
+ `,
809
+ successCard: `
810
+ background: linear-gradient(135deg, rgba(0,255,150,0.1) 0%, rgba(0,200,100,0.05) 100%);
811
+ border: 1px solid rgba(0,255,150,0.2);
812
+ border-radius: 12px;
813
+ padding: 20px 24px;
814
+ margin: 28px 0;
815
+ `,
816
+ successCardTitle: `
817
+ margin: 0 0 12px;
818
+ color: #00ff96;
819
+ font-size: 13px;
820
+ font-weight: 600;
821
+ text-transform: uppercase;
822
+ letter-spacing: 0.5px;
823
+ `,
824
+ linkSection: `
825
+ margin: 32px 0;
826
+ padding: 20px;
827
+ background-color: ${colors.subtle};
828
+ border-radius: 8px;
829
+ border: 1px solid ${colors.divider};
830
+ `,
831
+ linkLabel: `
832
+ margin: 0 0 8px;
833
+ color: ${colors.textMuted};
834
+ font-size: 12px;
835
+ text-transform: uppercase;
836
+ letter-spacing: 0.5px;
837
+ `,
838
+ linkText: `
839
+ word-break: break-all;
840
+ color: ${colors.textSecondary};
841
+ font-size: 13px;
842
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
843
+ margin: 0;
844
+ `,
845
+ divider: `
846
+ border: none;
847
+ border-top: 1px solid ${colors.divider};
848
+ margin: 32px 0;
849
+ `,
850
+ footer: `
851
+ padding: 24px 40px 32px;
852
+ text-align: center;
853
+ border-top: 1px solid ${colors.divider};
854
+ `,
855
+ footerText: `
856
+ margin: 0;
857
+ color: ${colors.textMuted};
858
+ font-size: 12px;
859
+ line-height: 1.8;
860
+ `,
861
+ footerLink: `
862
+ color: ${colors.textSecondary};
863
+ text-decoration: none;
864
+ `,
865
+ badge: `
866
+ display: inline-block;
867
+ background-color: rgba(255,255,255,0.1);
868
+ color: ${colors.textSecondary};
869
+ padding: 4px 12px;
870
+ border-radius: 20px;
871
+ font-size: 12px;
872
+ font-weight: 500;
873
+ letter-spacing: 0.3px;
874
+ `,
875
+ listItem: `
876
+ color: ${colors.textSecondary};
877
+ font-size: 14px;
878
+ margin: 8px 0;
879
+ padding-left: 8px;
880
+ `,
881
+ metaRow: `
882
+ display: flex;
883
+ justify-content: space-between;
884
+ padding: 12px 0;
885
+ border-bottom: 1px solid ${colors.divider};
886
+ `,
887
+ metaLabel: `
888
+ color: ${colors.textMuted};
889
+ font-size: 13px;
890
+ `,
891
+ metaValue: `
892
+ color: ${colors.textPrimary};
893
+ font-size: 13px;
894
+ font-weight: 500;
895
+ `
896
+ };
897
+ function buildVerificationEmailTemplate(data) {
898
+ const { firstName, verificationUrl, expiresIn } = data;
899
+ return `
900
+ <!DOCTYPE html>
901
+ <html lang="en">
902
+ <head>
903
+ <meta charset="UTF-8">
904
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
905
+ <meta name="color-scheme" content="dark">
906
+ <meta name="supported-color-schemes" content="dark">
907
+ <title>Verify Your Email</title>
908
+ <!--[if mso]>
909
+ <style type="text/css">
910
+ body, table, td {font-family: Arial, Helvetica, sans-serif !important;}
911
+ </style>
912
+ <![endif]-->
913
+ </head>
914
+ <body style="${styles.wrapper}">
915
+ <div style="${styles.container}">
916
+ <div style="${styles.card}">
917
+ <!-- Header -->
918
+ <div style="${styles.header}">
919
+ <div style="${styles.iconWrapper}">
920
+ \u2709\uFE0F
921
+ </div>
922
+ <h1 style="${styles.headerTitle}">Verify your email</h1>
923
+ <p style="${styles.headerSubtitle}">One quick step to get started</p>
924
+ </div>
925
+
926
+ <!-- Body -->
927
+ <div style="${styles.body}">
928
+ <p style="${styles.greeting}">Hi ${firstName},</p>
929
+
930
+ <p style="${styles.paragraph}">
931
+ Thanks for signing up. To complete your registration and unlock all features,
932
+ please verify your email address by clicking the button below.
933
+ </p>
934
+
935
+ <div style="${styles.buttonWrapper}">
936
+ <a href="${verificationUrl}" style="${styles.button}" target="_blank">
937
+ Verify Email Address
938
+ </a>
939
+ </div>
940
+
941
+ <div style="${styles.infoCard}">
942
+ <p style="${styles.infoCardTitle}">\u23F1 Time Sensitive</p>
943
+ <p style="${styles.infoCardText}">
944
+ This verification link will expire in <strong>${expiresIn}</strong>.
945
+ If you didn't create an account, you can safely ignore this email.
946
+ </p>
947
+ </div>
948
+ </div>
949
+
950
+ <!-- Footer -->
951
+ <div style="${styles.footer}">
952
+ <p style="${styles.footerText}">
953
+ This is an automated message \u2014 please do not reply.<br>
954
+ \xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} All rights reserved.
955
+ </p>
956
+ </div>
957
+ </div>
958
+ </div>
959
+ </body>
960
+ </html>
961
+ `;
962
+ }
963
+ function buildResetPasswordEmailTemplate(data) {
964
+ const { firstName, resetUrl, expiresIn } = data;
965
+ return `
966
+ <!DOCTYPE html>
967
+ <html lang="en">
968
+ <head>
969
+ <meta charset="UTF-8">
970
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
971
+ <meta name="color-scheme" content="dark">
972
+ <meta name="supported-color-schemes" content="dark">
973
+ <title>Reset Your Password</title>
974
+ </head>
975
+ <body style="${styles.wrapper}">
976
+ <div style="${styles.container}">
977
+ <div style="${styles.card}">
978
+ <!-- Header -->
979
+ <div style="${styles.header}">
980
+ <div style="${styles.iconWrapper}">
981
+ \u{1F510}
982
+ </div>
983
+ <h1 style="${styles.headerTitle}">Reset your password</h1>
984
+ <p style="${styles.headerSubtitle}">We received a reset request</p>
985
+ </div>
986
+
987
+ <!-- Body -->
988
+ <div style="${styles.body}">
989
+ <p style="${styles.greeting}">Hi ${firstName},</p>
990
+
991
+ <p style="${styles.paragraph}">
992
+ We received a request to reset the password for your account.
993
+ Click the button below to create a new password.
994
+ </p>
995
+
996
+ <div style="${styles.buttonWrapper}">
997
+ <a href="${resetUrl}" style="${styles.button}" target="_blank">
998
+ Reset Password
999
+ </a>
1000
+ </div>
1001
+
1002
+ <div style="${styles.warningCard}">
1003
+ <p style="${styles.warningCardTitle}">\u26A0\uFE0F Security Notice</p>
1004
+ <p style="${styles.warningCardText}">
1005
+ \u2022 This link expires in <strong>${expiresIn}</strong><br>
1006
+ \u2022 This link can only be used once<br>
1007
+ \u2022 If you didn't request this, ignore this email
1008
+ </p>
1009
+ </div>
1010
+
1011
+ <hr style="${styles.divider}" />
1012
+
1013
+ <p style="${styles.paragraph}; font-size: 13px; color: ${colors.textMuted};">
1014
+ <strong>Didn't request this?</strong><br>
1015
+ Your password remains unchanged. If you're concerned about your account
1016
+ security, please contact our support team immediately.
1017
+ </p>
1018
+ </div>
1019
+
1020
+ <!-- Footer -->
1021
+ <div style="${styles.footer}">
1022
+ <p style="${styles.footerText}">
1023
+ This is an automated message \u2014 please do not reply.<br>
1024
+ \xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} All rights reserved.
1025
+ </p>
1026
+ </div>
1027
+ </div>
1028
+ </div>
1029
+ </body>
1030
+ </html>
1031
+ `;
1032
+ }
1033
+
667
1034
  // src/express/auth.routes.ts
668
1035
  function createAuthRouter(options = {}) {
669
1036
  const googleClientId = process.env.GOOGLE_CLIENT_ID;
@@ -685,7 +1052,7 @@ function createAuthRouter(options = {}) {
685
1052
  // default: secure in prod
686
1053
  domain: options.cookie?.domain ?? void 0,
687
1054
  path: options.cookie?.path ?? "/",
688
- maxAgeMs: options.cookie?.maxAgeMs ?? 24 * 60 * 60 * 1e3
1055
+ maxAgeMs: options.cookie?.maxAgeMs ?? 30 * 24 * 60 * 60 * 1e3
689
1056
  };
690
1057
  r.use(import_express.default.json());
691
1058
  r.use(import_express.default.urlencoded({ extended: true }));
@@ -742,6 +1109,7 @@ function createAuthRouter(options = {}) {
742
1109
  projectId,
743
1110
  metadata
744
1111
  } = req.body || {};
1112
+ const COMPANY_NAME = process.env.COMPANY_NAME;
745
1113
  try {
746
1114
  const kcUser = await authAdmin.createUserInRealm({
747
1115
  username: emailAddress,
@@ -770,10 +1138,21 @@ function createAuthRouter(options = {}) {
770
1138
  emailService: email,
771
1139
  user,
772
1140
  subject: "Verify your email",
773
- html: buildVerificationTemplate(
774
- email.sign({ userId: kcUser.id, email: kcUser.email }),
775
- options
776
- )
1141
+ // html: buildVerificationTemplate(
1142
+ // email.sign({ userId: kcUser.id, email: kcUser.email }),
1143
+ // options,
1144
+ // ),
1145
+ html: buildVerificationEmailTemplate({
1146
+ firstName: user.firstName,
1147
+ verificationUrl: `${getFrontendBaseUrl(options)}/verify-email?token=${email.sign(
1148
+ {
1149
+ userId: user.id,
1150
+ email: user.email
1151
+ }
1152
+ )}`,
1153
+ expiresIn: "1 hour"
1154
+ }),
1155
+ from: COMPANY_NAME
777
1156
  });
778
1157
  if (emailResult.rateLimited) {
779
1158
  return res.status(429).json({
@@ -838,6 +1217,7 @@ function createAuthRouter(options = {}) {
838
1217
  "/resend-verification-email",
839
1218
  validateResendEmail,
840
1219
  async (req, res) => {
1220
+ const COMPANY_NAME = process.env.COMPANY_NAME;
841
1221
  const user = await OrgUser.findOne({ email: req.body.email });
842
1222
  if (!user)
843
1223
  return res.status(404).json({ ok: false, error: "User not found" });
@@ -853,7 +1233,18 @@ function createAuthRouter(options = {}) {
853
1233
  emailService: email,
854
1234
  user,
855
1235
  subject: "Verify your email",
856
- html: buildVerificationTemplate(token, options)
1236
+ // html: buildVerificationTemplate(token, options),
1237
+ html: buildVerificationEmailTemplate({
1238
+ firstName: user.firstName,
1239
+ verificationUrl: `${getFrontendBaseUrl(options)}/verify-email?token=${email.sign(
1240
+ {
1241
+ userId: user.id,
1242
+ email: user.email
1243
+ }
1244
+ )}`,
1245
+ expiresIn: "1 hour"
1246
+ }),
1247
+ from: COMPANY_NAME
857
1248
  });
858
1249
  if (resendResult.rateLimited) {
859
1250
  return res.status(429).json({
@@ -866,6 +1257,7 @@ function createAuthRouter(options = {}) {
866
1257
  }
867
1258
  );
868
1259
  r.post("/forgot-password", validateResendEmail, async (req, res) => {
1260
+ const COMPANY_NAME = process.env.COMPANY_NAME;
869
1261
  const user = await OrgUser.findOne({ email: req.body.email });
870
1262
  if (!user)
871
1263
  return res.status(404).json({ ok: false, error: "User not found" });
@@ -882,7 +1274,18 @@ function createAuthRouter(options = {}) {
882
1274
  emailService: email,
883
1275
  user,
884
1276
  subject: "Reset password",
885
- html: buildResetTemplate(resetToken, options)
1277
+ // html: buildResetTemplate(resetToken, options),
1278
+ html: buildResetPasswordEmailTemplate({
1279
+ firstName: user.firstName,
1280
+ resetUrl: `${getFrontendBaseUrl(options)}/reset-password?token=${email.sign(
1281
+ {
1282
+ userId: user.id,
1283
+ email: user.email
1284
+ }
1285
+ )}`,
1286
+ expiresIn: "1 hour"
1287
+ }),
1288
+ from: COMPANY_NAME
886
1289
  });
887
1290
  if (resetResult.rateLimited) {
888
1291
  return res.status(429).json({
@@ -895,9 +1298,16 @@ function createAuthRouter(options = {}) {
895
1298
  });
896
1299
  r.post("/reset-password", validateResetPassword, async (req, res) => {
897
1300
  const { token, newPassword } = req.body || {};
1301
+ if (!token || !newPassword) {
1302
+ return res.status(400).json({
1303
+ ok: false,
1304
+ error: "Token and new password are required",
1305
+ code: "MISSING_FIELDS"
1306
+ });
1307
+ }
898
1308
  try {
899
1309
  const payload = email.verify(token);
900
- const user = await OrgUser.findOne({ keycloakId: payload.userId });
1310
+ const user = await OrgUser.findOne({ id: payload.userId });
901
1311
  if (!user) {
902
1312
  return res.status(404).json({ ok: false, error: "User not found" });
903
1313
  }
@@ -1265,8 +1675,6 @@ function setAuthCookies(res, tokens, cookie) {
1265
1675
  if (cookie.domain) {
1266
1676
  base.domain = cookie.domain;
1267
1677
  }
1268
- console.log(cookie, "cookie");
1269
- console.log(base, "base");
1270
1678
  if (tokens?.access_token) {
1271
1679
  res.cookie("access_token", tokens.access_token, base);
1272
1680
  }
@@ -1290,12 +1698,6 @@ function respondWithKeycloakError(res, err, fallback, status = 400) {
1290
1698
  const description = err?.response?.data?.error_description || err?.response?.data?.errorMessage || err?.message || fallback;
1291
1699
  return res.status(status).json({ ok: false, error: description });
1292
1700
  }
1293
- function buildVerificationTemplate(token, options) {
1294
- return `<a href="${getFrontendBaseUrl(options)}/auth/verify-email?token=${token}">Verify</a>`;
1295
- }
1296
- function buildResetTemplate(token, options) {
1297
- return `<a href="${getFrontendBaseUrl(options)}/auth/reset-password?token=${token}">Reset</a>`;
1298
- }
1299
1701
  function getFrontendBaseUrl(options) {
1300
1702
  if (options.frontendBaseUrl)
1301
1703
  return options.frontendBaseUrl.replace(/\/$/, "");
@@ -1307,13 +1709,14 @@ async function sendRateLimitedEmail({
1307
1709
  emailService,
1308
1710
  user,
1309
1711
  subject,
1310
- html
1712
+ html,
1713
+ from
1311
1714
  }) {
1312
1715
  const can = emailService.canSend(user?.lastEmailSent || []);
1313
1716
  if (!can.ok) {
1314
1717
  return { rateLimited: true, waitMs: can.waitMs };
1315
1718
  }
1316
- await emailService.send(user.email, subject, html);
1719
+ await emailService.send(user.email, subject, html, from);
1317
1720
  user.lastEmailSent = [...user.lastEmailSent || [], /* @__PURE__ */ new Date()];
1318
1721
  await user.save();
1319
1722
  return { rateLimited: false };
@@ -1334,7 +1737,7 @@ function generateTokens(user) {
1334
1737
  type: "user"
1335
1738
  };
1336
1739
  const accessToken = import_jsonwebtoken4.default.sign(accessPayload, process.env.JWT_SECRET, {
1337
- expiresIn: "1h"
1740
+ expiresIn: "1d"
1338
1741
  });
1339
1742
  const refreshToken = import_jsonwebtoken4.default.sign(
1340
1743
  { sub: user._id.toString() },
@@ -1370,13 +1773,61 @@ function createDashboardRouter(options) {
1370
1773
  }
1371
1774
 
1372
1775
  // src/express/email.routes.ts
1373
- var import_express3 = require("express");
1776
+ var import_express3 = __toESM(require("express"), 1);
1374
1777
  function createEmailRouter(options) {
1375
1778
  const r = (0, import_express3.Router)();
1779
+ const emailService = new EmailService();
1780
+ r.use(import_express3.default.json());
1781
+ r.use(import_express3.default.urlencoded({ extended: true }));
1376
1782
  r.get(
1377
1783
  "/verify",
1378
1784
  (req, res) => res.json({ ok: true, token: req.query.token })
1379
1785
  );
1786
+ r.post("/send", async (req, res) => {
1787
+ try {
1788
+ const { userId, to, subject, html, from } = req.body ?? {};
1789
+ if (!to || !subject || !html) {
1790
+ return res.status(400).json({
1791
+ ok: false,
1792
+ error: "BAD_REQUEST",
1793
+ message: "`to`, `subject`, and `html` are required."
1794
+ });
1795
+ }
1796
+ if (userId) {
1797
+ const user = await OrgUser.findOne({ id: userId }).lean();
1798
+ if (!user) {
1799
+ return res.status(404).json({
1800
+ ok: false,
1801
+ error: "NOT_FOUND",
1802
+ message: "User not found."
1803
+ });
1804
+ }
1805
+ const can = emailService.canSend(user?.lastEmailSent || []);
1806
+ if (!can.ok) {
1807
+ return res.status(429).json({
1808
+ ok: false,
1809
+ error: can.reason,
1810
+ waitMs: can.waitMs,
1811
+ message: "Too many emails sent recently. Please retry later."
1812
+ });
1813
+ }
1814
+ }
1815
+ await emailService.send(to, subject, html, from);
1816
+ if (userId) {
1817
+ await OrgUser.updateOne(
1818
+ { id: userId },
1819
+ { $push: { lastEmailSent: /* @__PURE__ */ new Date() } }
1820
+ );
1821
+ }
1822
+ return res.json({ ok: true });
1823
+ } catch (err) {
1824
+ return res.status(500).json({
1825
+ ok: false,
1826
+ error: "INTERNAL",
1827
+ message: err?.message ?? "Error"
1828
+ });
1829
+ }
1830
+ });
1380
1831
  return r;
1381
1832
  }
1382
1833