prostgles-server 4.2.155 → 4.2.157

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.
@@ -8,6 +8,7 @@ import type { StrategyOptions as GoogleStrategy, Profile as GoogleProfile } from
8
8
  import type { StrategyOptions as GitHubStrategy, Profile as GitHubProfile } from "passport-github2";
9
9
  import type { MicrosoftStrategyOptions } from "passport-microsoft";
10
10
  import type { StrategyOptions as FacebookStrategy, Profile as FacebookProfile } from "passport-facebook";
11
+ import Mail from "nodemailer/lib/mailer";
11
12
 
12
13
  type Awaitable<T> = T | Promise<T>;
13
14
 
@@ -49,30 +50,62 @@ type ThirdPartyProviders = {
49
50
  };
50
51
  };
51
52
 
52
- type SMTPConfig = {
53
- type: "aws-ses" | "smtp";
53
+ export type SMTPConfig = {
54
+ type: "smtp";
54
55
  host: string;
55
56
  port: number;
56
57
  secure: boolean;
57
- auth: {
58
- user: string;
59
- pass: string;
60
- }
58
+ user: string;
59
+ pass: string;
60
+ } | {
61
+ type: "aws-ses";
62
+ region: string;
63
+ accessKeyId: string;
64
+ secretAccessKey: string;
65
+ /**
66
+ * Sending rate per second
67
+ * Defaults to 1
68
+ */
69
+ sendingRate?: number;
61
70
  }
62
71
 
63
- type RegistrationProviders = ThirdPartyProviders & {
64
- email?: {
65
- signupType: "withMagicLink" | "withPassword";
66
- smtp: SMTPConfig
67
- } | {
68
- signupType: "withPassword";
69
- /**
70
- * If provided, the user will be required to confirm their email address
71
- */
72
- smtp?: SMTPConfig;
73
- };
72
+ export type Email = {
73
+ from: string;
74
+ to: string;
75
+ subject: string;
76
+ html: string;
77
+ text?: string;
78
+ attachments?: { filename: string; content: string; }[] | Mail.Attachment[];
74
79
  }
75
80
 
81
+ type EmailWithoutTo = Omit<Email, "to">;
82
+
83
+ type EmailProvider =
84
+ | {
85
+ signupType: "withMagicLink";
86
+ onRegistered: (data: { username: string; }) => void | Promise<void>;
87
+ emailMagicLink: {
88
+ onSend: (data: { email: string; magicLinkPath: string; }) => EmailWithoutTo | Promise<EmailWithoutTo>;
89
+ smtp: SMTPConfig;
90
+ };
91
+ }
92
+ | {
93
+ signupType: "withPassword";
94
+ onRegistered: (data: { username: string; password: string; }) => void | Promise<void>;
95
+ /**
96
+ * Defaults to 8
97
+ */
98
+ minPasswordLength: number;
99
+ /**
100
+ * If provided, the user will be required to confirm their email address
101
+ */
102
+ emailConfirmation?: {
103
+ onSend: (data: { email: string; confirmationUrlPath: string; }) => EmailWithoutTo | Promise<EmailWithoutTo>;
104
+ smtp: SMTPConfig;
105
+ onConfirmed: (data: { confirmationUrlPath: string; }) => void | Promise<void>;
106
+ };
107
+ };
108
+
76
109
  export type AuthProviderUserData =
77
110
  | {
78
111
  provider: "google";
@@ -109,7 +142,11 @@ export type RegistrationData =
109
142
  }
110
143
  | AuthProviderUserData;
111
144
 
112
- export type AuthRegistrationConfig<S> = RegistrationProviders & {
145
+ export type AuthRegistrationConfig<S> = {
146
+ email?: EmailProvider;
147
+
148
+ OAuthProviders?: ThirdPartyProviders;
149
+
113
150
  /**
114
151
  * Required for social login callback
115
152
  */
@@ -0,0 +1,83 @@
1
+ import { Email, SMTPConfig } from "./AuthTypes";
2
+ import nodemailer from "nodemailer";
3
+ import aws from "@aws-sdk/client-ses";
4
+ import SESTransport from "nodemailer/lib/ses-transport";
5
+
6
+ type SESTransporter = nodemailer.Transporter<SESTransport.SentMessageInfo, SESTransport.Options>;
7
+ type SMTPTransporter = nodemailer.Transporter<nodemailer.SentMessageInfo, nodemailer.TransportOptions>;
8
+ type Transporter = SESTransporter | SMTPTransporter;
9
+
10
+ const transporterCache: Map<string, Transporter> = new Map();
11
+
12
+ /**
13
+ * Allows sending emails using nodemailer default config or AWS SES
14
+ * https://www.nodemailer.com/transports/ses/
15
+ */
16
+ export const sendEmail = (smptConfig: SMTPConfig, email: Email) => {
17
+ const configStr = JSON.stringify(smptConfig);
18
+ const transporter = transporterCache.get(configStr) ?? getTransporter(smptConfig);
19
+ if(!transporterCache.has(configStr)){
20
+ transporterCache.set(configStr, transporter);
21
+ }
22
+
23
+ return send(transporter, email);
24
+ }
25
+
26
+ const getTransporter = (smptConfig: SMTPConfig) => {
27
+ let transporter: Transporter | undefined;
28
+ if(smptConfig.type === "aws-ses"){
29
+ const {
30
+ region,
31
+ accessKeyId,
32
+ secretAccessKey,
33
+ /**
34
+ * max 1 messages/second
35
+ */
36
+ sendingRate = 1
37
+ } = smptConfig;
38
+ const ses = new aws.SES({
39
+ apiVersion: "2010-12-01",
40
+ region,
41
+ credentials: {
42
+ accessKeyId,
43
+ secretAccessKey
44
+ }
45
+ });
46
+
47
+ transporter = nodemailer.createTransport({
48
+ SES: { ses, aws },
49
+ maxConnections: 1,
50
+ sendingRate
51
+ });
52
+
53
+ } else {
54
+ const { user, pass, host, port, secure } = smptConfig;
55
+ transporter = nodemailer.createTransport({
56
+ host,
57
+ port,
58
+ secure,
59
+ auth: { user, pass }
60
+ });
61
+ }
62
+
63
+ return transporter;
64
+ }
65
+
66
+ const send = (transporter: Transporter, email: Email) => {
67
+ return new Promise((resolve, reject) => {
68
+ transporter.once('idle', () => {
69
+ if (transporter.isIdle()) {
70
+ transporter.sendMail(
71
+ email,
72
+ (err, info) => {
73
+ if(err){
74
+ reject(err);
75
+ } else {
76
+ resolve(info);
77
+ }
78
+ }
79
+ );
80
+ }
81
+ });
82
+ });
83
+ };
@@ -1,17 +1,17 @@
1
- import { Auth } from './AuthTypes';
2
- /** For some reason normal import is undefined */
3
- const passport = require("passport") as typeof import("passport");
4
- import { Strategy as GoogleStrategy } from "passport-google-oauth20";
5
- import { Strategy as GitHubStrategy } from "passport-github2";
6
- import { Strategy as MicrosoftStrategy } from "passport-microsoft";
7
- import { Strategy as FacebookStrategy } from "passport-facebook";
8
- import { AuthSocketSchema, getKeys, isDefined, isEmpty } from "prostgles-types";
9
- import { AUTH_ROUTES_AND_PARAMS, AuthHandler, getLoginClientInfo } from "./AuthHandler";
10
1
  import type e from "express";
11
2
  import { RequestHandler } from "express";
12
- import { removeExpressRouteByName } from "../FileManager/FileManager";
3
+ import { Strategy as FacebookStrategy } from "passport-facebook";
4
+ import { Strategy as GitHubStrategy } from "passport-github2";
5
+ import { Strategy as GoogleStrategy } from "passport-google-oauth20";
6
+ import { Strategy as MicrosoftStrategy } from "passport-microsoft";
7
+ import { AuthSocketSchema, getObjectEntries, isEmpty } from "prostgles-types";
13
8
  import { getErrorAsObject } from "../DboBuilder/dboBuilderUtils";
14
-
9
+ import { removeExpressRouteByName } from "../FileManager/FileManager";
10
+ import { AUTH_ROUTES_AND_PARAMS, AuthHandler, getLoginClientInfo } from "./AuthHandler";
11
+ import { Auth } from './AuthTypes';
12
+ import { setEmailProvider } from "./setEmailProvider";
13
+ /** For some reason normal import is undefined */
14
+ const passport = require("passport") as typeof import("passport");
15
15
 
16
16
  export const upsertNamedExpressMiddleware = (app: e.Express, handler: RequestHandler, name: string) => {
17
17
  const funcName = name;
@@ -22,56 +22,36 @@ export const upsertNamedExpressMiddleware = (app: e.Express, handler: RequestHan
22
22
 
23
23
  export function setAuthProviders (this: AuthHandler, { registrations, app }: Required<Auth>["expressConfig"]) {
24
24
  if(!registrations) return;
25
- const { email, onRegister, onProviderLoginFail, onProviderLoginStart, websiteUrl, ...providers } = registrations;
26
- if(email){
27
- app.post(AUTH_ROUTES_AND_PARAMS.emailSignup, async (req, res) => {
28
- const { username, password } = req.body;
29
- if(typeof username !== "string" || typeof password !== "string"){
30
- res.status(400).json({ msg: "Invalid username or password" });
31
- return;
32
- }
33
- await onRegister({ provider: "email", profile: { username, password }});
34
- })
35
- }
25
+ const { onRegister, onProviderLoginFail, onProviderLoginStart, websiteUrl, OAuthProviders } = registrations;
26
+
27
+ setEmailProvider.bind(this)(app);
36
28
 
37
- if(!isEmpty(providers)){
38
- upsertNamedExpressMiddleware(app, passport.initialize(), "prostglesPassportMiddleware");
29
+ if(!OAuthProviders || isEmpty(OAuthProviders)){
30
+ return;
39
31
  }
40
32
 
41
- ([
42
- providers.google && {
43
- providerName: "google" as const,
44
- config: providers.google,
45
- strategy: GoogleStrategy,
46
- },
47
- providers.github && {
48
- providerName: "github" as const,
49
- config: providers.github,
50
- strategy: GitHubStrategy,
51
- },
52
- providers.facebook && {
53
- providerName: "facebook" as const,
54
- config: providers.facebook,
55
- strategy: FacebookStrategy,
56
- },
57
- providers.microsoft && {
58
- providerName: "microsoft" as const,
59
- config: providers.microsoft,
60
- strategy: MicrosoftStrategy,
33
+ upsertNamedExpressMiddleware(app, passport.initialize(), "prostglesPassportMiddleware");
34
+
35
+ getObjectEntries(OAuthProviders).forEach(([providerName, providerConfig]) => {
36
+
37
+ if(!providerConfig?.clientID){
38
+ return;
61
39
  }
62
- ])
63
- .filter(isDefined)
64
- .forEach(({
65
- config: { authOpts, ...config },
66
- strategy,
67
- providerName,
68
- }) => {
40
+
41
+ const { authOpts, ...config } = providerConfig;
42
+
43
+ const strategy = providerName === "google" ? GoogleStrategy :
44
+ providerName === "github" ? GitHubStrategy :
45
+ providerName === "facebook" ? FacebookStrategy :
46
+ providerName === "microsoft" ? MicrosoftStrategy :
47
+ undefined
48
+ ;
69
49
 
70
50
  const callbackPath = `${AUTH_ROUTES_AND_PARAMS.loginWithProvider}/${providerName}/callback`;
71
51
  passport.use(
72
52
  new (strategy as typeof GoogleStrategy)(
73
53
  {
74
- ...config as any,
54
+ ...config,
75
55
  callbackURL: `${websiteUrl}${callbackPath}`,
76
56
  },
77
57
  async (accessToken, refreshToken, profile, done) => {
@@ -92,8 +72,9 @@ export function setAuthProviders (this: AuthHandler, { registrations, app }: Req
92
72
  try {
93
73
  const clientInfo = getLoginClientInfo({ httpReq: req });
94
74
  const db = this.db;
95
- const dbo = this.dbo as any
96
- const startCheck = await onProviderLoginStart({ provider: providerName, req, res, clientInfo, db, dbo });
75
+ const dbo = this.dbo as any;
76
+ const args = { provider: providerName, req, res, clientInfo, db, dbo };
77
+ const startCheck = await onProviderLoginStart(args);
97
78
  if("error" in startCheck){
98
79
  res.status(500).json({ error: startCheck.error });
99
80
  return;
@@ -107,7 +88,7 @@ export function setAuthProviders (this: AuthHandler, { registrations, app }: Req
107
88
  },
108
89
  async (error: any, _profile: any, authInfo: any) => {
109
90
  if(error){
110
- await onProviderLoginFail({ provider: providerName, error, req, res, clientInfo, db, dbo });
91
+ await onProviderLoginFail({ ...args, error });
111
92
  res.status(500).json({
112
93
  error: "Failed to login with provider",
113
94
  });
@@ -120,7 +101,7 @@ export function setAuthProviders (this: AuthHandler, { registrations, app }: Req
120
101
  }
121
102
  )(req, res);
122
103
 
123
- } catch (e) {
104
+ } catch (_e) {
124
105
  res.status(500).json({ error: "Something went wrong" });
125
106
  }
126
107
  }
@@ -132,16 +113,12 @@ export function setAuthProviders (this: AuthHandler, { registrations, app }: Req
132
113
  export function getProviders(this: AuthHandler): AuthSocketSchema["providers"] | undefined {
133
114
  const { registrations } = this.opts?.expressConfig ?? {}
134
115
  if(!registrations) return undefined;
135
- const {
136
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
137
- email, websiteUrl, onRegister, onProviderLoginFail, onProviderLoginStart,
138
- ...providers
139
- } = registrations;
140
- if(isEmpty(providers)) return undefined;
116
+ const { OAuthProviders } = registrations;
117
+ if(!OAuthProviders || isEmpty(OAuthProviders)) return undefined;
141
118
 
142
119
  const result: AuthSocketSchema["providers"] = {}
143
- getKeys(providers).forEach(providerName => {
144
- if(providers[providerName]?.clientID){
120
+ getObjectEntries(OAuthProviders).forEach(([providerName, config]) => {
121
+ if(config?.clientID){
145
122
  result[providerName] = {
146
123
  url: `${AUTH_ROUTES_AND_PARAMS.loginWithProvider}/${providerName}`,
147
124
  }
@@ -0,0 +1,63 @@
1
+ import e from "express";
2
+ import { AUTH_ROUTES_AND_PARAMS, AuthHandler } from "./AuthHandler";
3
+ import { Email, SMTPConfig } from "./AuthTypes";
4
+ import { sendEmail } from "./sendEmail";
5
+
6
+ export function setEmailProvider(this: AuthHandler, app: e.Express) {
7
+
8
+ const { email, websiteUrl } = this.opts?.expressConfig?.registrations ?? {};
9
+ if(!email) return;
10
+
11
+ app.post(AUTH_ROUTES_AND_PARAMS.emailSignup, async (req, res) => {
12
+ const { username, password } = req.body;
13
+ let validationError = "";
14
+ if(typeof username !== "string"){
15
+ validationError = "Invalid username";
16
+ }
17
+ if(email.signupType === "withPassword"){
18
+ const { minPasswordLength = 8 } = email;
19
+ if(typeof password !== "string"){
20
+ validationError = "Invalid password";
21
+ } else if(password.length < minPasswordLength){
22
+ validationError = `Password must be at least ${minPasswordLength} characters long`;
23
+ }
24
+ }
25
+ if(validationError){
26
+ res.status(400).json({ error: validationError });
27
+ return;
28
+ }
29
+ try {
30
+ let emailMessage: undefined | { message: Email; smtp: SMTPConfig };
31
+ if(email.signupType === "withPassword"){
32
+ if(email.emailConfirmation){
33
+ const { onSend, smtp } = email.emailConfirmation;
34
+ const message = await onSend({ email: username, confirmationUrlPath: `${websiteUrl}${AUTH_ROUTES_AND_PARAMS.confirmEmail}` });
35
+ emailMessage = { message: { ...message, to: username }, smtp };
36
+ }
37
+ } else {
38
+ const { emailMagicLink } = email;
39
+ const message = await emailMagicLink.onSend({ email: username, magicLinkPath: `${websiteUrl}${AUTH_ROUTES_AND_PARAMS.magicLinksRoute}` });
40
+ emailMessage = { message: { ...message, to: username }, smtp: emailMagicLink.smtp };
41
+ }
42
+
43
+ if(emailMessage){
44
+ await sendEmail(emailMessage.smtp, emailMessage.message);
45
+ res.json({ msg: "Email sent" });
46
+ }
47
+ } catch {
48
+ res.status(500).json({ error: "Failed to send email" });
49
+ }
50
+ });
51
+
52
+ if(email.signupType === "withPassword" && email.emailConfirmation){
53
+ app.get(AUTH_ROUTES_AND_PARAMS.confirmEmailExpressRoute, async (req, res) => {
54
+ const { id } = req.params ?? {};
55
+ try {
56
+ await email.emailConfirmation?.onConfirmed({ confirmationUrlPath: id });
57
+ res.json({ msg: "Email confirmed" });
58
+ } catch (_e) {
59
+ res.status(500).json({ error: "Failed to confirm email" });
60
+ }
61
+ });
62
+ }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prostgles-server",
3
- "version": "4.2.155",
3
+ "version": "4.2.157",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -35,9 +35,17 @@
35
35
  ],
36
36
  "homepage": "https://prostgles.com",
37
37
  "dependencies": {
38
+ "@aws-sdk/client-ses": "^3.699.0",
39
+ "@aws-sdk/credential-provider-node": "^3.699.0",
40
+ "@types/passport": "^1.0.17",
41
+ "@types/passport-facebook": "^3.0.3",
42
+ "@types/passport-github2": "^1.2.9",
43
+ "@types/passport-google-oauth20": "^2.0.16",
44
+ "@types/passport-microsoft": "^1.0.3",
38
45
  "body-parser": "^1.20.3",
39
46
  "check-disk-space": "^3.4.0",
40
47
  "file-type": "^18.5.0",
48
+ "nodemailer": "^6.9.16",
41
49
  "passport": "^0.7.0",
42
50
  "passport-facebook": "^3.0.0",
43
51
  "passport-github2": "^0.1.12",
@@ -46,17 +54,13 @@
46
54
  "pg": "^8.11.5",
47
55
  "pg-cursor": "^2.11.0",
48
56
  "pg-promise": "^11.9.1",
49
- "prostgles-types": "^4.0.105"
57
+ "prostgles-types": "^4.0.107"
50
58
  },
51
59
  "devDependencies": {
52
60
  "@types/express": "^4.17.21",
53
61
  "@types/json-schema": "^7.0.15",
54
62
  "@types/node": "^22.8.1",
55
- "@types/passport": "^1.0.17",
56
- "@types/passport-facebook": "^3.0.3",
57
- "@types/passport-github2": "^1.2.9",
58
- "@types/passport-google-oauth20": "^2.0.16",
59
- "@types/passport-microsoft": "^1.0.3",
63
+ "@types/nodemailer": "^6.4.17",
60
64
  "@types/pg": "^8.11.5",
61
65
  "@types/pg-cursor": "^2.7.2",
62
66
  "@types/sharp": "^0.30.4",