Skip to content

Commit

Permalink
imrpove auth config
Browse files Browse the repository at this point in the history
  • Loading branch information
prostgles committed Nov 27, 2024
1 parent 2ef3b35 commit f56d70a
Show file tree
Hide file tree
Showing 11 changed files with 2,598 additions and 2,799 deletions.
6 changes: 4 additions & 2 deletions lib/Auth/AuthHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const AUTH_ROUTES_AND_PARAMS = {
logoutGetPath: "/logout",
magicLinksRoute: "/magic-link",
magicLinksExpressRoute: "/magic-link/:id",
confirmEmail: "/confirm-email",
confirmEmailExpressRoute: "/confirm-email/:id",
catchAll: "*",
} as const;

Expand Down Expand Up @@ -160,8 +162,8 @@ export class AuthHandler {

destroy = () => {
const app = this.opts?.expressConfig?.app;
const { login, logoutGetPath, magicLinksExpressRoute, catchAll, loginWithProvider, emailSignup, magicLinksRoute } = AUTH_ROUTES_AND_PARAMS;
removeExpressRoute(app, [login, logoutGetPath, magicLinksExpressRoute, catchAll, loginWithProvider, emailSignup, magicLinksRoute]);
const { login, logoutGetPath, magicLinksExpressRoute, catchAll, loginWithProvider, emailSignup, magicLinksRoute, confirmEmail } = AUTH_ROUTES_AND_PARAMS;
removeExpressRoute(app, [login, logoutGetPath, magicLinksExpressRoute, catchAll, loginWithProvider, emailSignup, magicLinksRoute, confirmEmail]);
}

throttledFunc = <T>(func: () => Promise<T>, throttle = 500): Promise<T> => {
Expand Down
73 changes: 55 additions & 18 deletions lib/Auth/AuthTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { StrategyOptions as GoogleStrategy, Profile as GoogleProfile } from
import type { StrategyOptions as GitHubStrategy, Profile as GitHubProfile } from "passport-github2";
import type { MicrosoftStrategyOptions } from "passport-microsoft";
import type { StrategyOptions as FacebookStrategy, Profile as FacebookProfile } from "passport-facebook";
import Mail from "nodemailer/lib/mailer";

type Awaitable<T> = T | Promise<T>;

Expand Down Expand Up @@ -49,30 +50,62 @@ type ThirdPartyProviders = {
};
};

type SMTPConfig = {
type: "aws-ses" | "smtp";
export type SMTPConfig = {
type: "smtp";
host: string;
port: number;
secure: boolean;
auth: {
user: string;
pass: string;
}
user: string;
pass: string;
} | {
type: "aws-ses";
region: string;
accessKeyId: string;
secretAccessKey: string;
/**
* Sending rate per second
* Defaults to 1
*/
sendingRate?: number;
}

type RegistrationProviders = ThirdPartyProviders & {
email?: {
signupType: "withMagicLink" | "withPassword";
smtp: SMTPConfig
} | {
signupType: "withPassword";
/**
* If provided, the user will be required to confirm their email address
*/
smtp?: SMTPConfig;
};
export type Email = {
from: string;
to: string;
subject: string;
html: string;
text?: string;
attachments?: { filename: string; content: string; }[] | Mail.Attachment[];
}

type EmailWithoutTo = Omit<Email, "to">;

type EmailProvider =
| {
signupType: "withMagicLink";
onRegistered: (data: { username: string; }) => void | Promise<void>;
emailMagicLink: {
onSend: (data: { email: string; magicLinkPath: string; }) => EmailWithoutTo | Promise<EmailWithoutTo>;
smtp: SMTPConfig;
};
}
| {
signupType: "withPassword";
onRegistered: (data: { username: string; password: string; }) => void | Promise<void>;
/**
* Defaults to 8
*/
minPasswordLength: number;
/**
* If provided, the user will be required to confirm their email address
*/
emailConfirmation?: {
onSend: (data: { email: string; confirmationUrlPath: string; }) => EmailWithoutTo | Promise<EmailWithoutTo>;
smtp: SMTPConfig;
onConfirmed: (data: { confirmationUrlPath: string; }) => void | Promise<void>;
};
};

export type AuthProviderUserData =
| {
provider: "google";
Expand Down Expand Up @@ -109,7 +142,11 @@ export type RegistrationData =
}
| AuthProviderUserData;

export type AuthRegistrationConfig<S> = RegistrationProviders & {
export type AuthRegistrationConfig<S> = {
email?: EmailProvider;

OAuthProviders?: ThirdPartyProviders;

/**
* Required for social login callback
*/
Expand Down
83 changes: 83 additions & 0 deletions lib/Auth/sendEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Email, SMTPConfig } from "./AuthTypes";
import nodemailer from "nodemailer";
import aws from "@aws-sdk/client-ses";
import SESTransport from "nodemailer/lib/ses-transport";

type SESTransporter = nodemailer.Transporter<SESTransport.SentMessageInfo, SESTransport.Options>;
type SMTPTransporter = nodemailer.Transporter<nodemailer.SentMessageInfo, nodemailer.TransportOptions>;
type Transporter = SESTransporter | SMTPTransporter;

const transporterCache: Map<string, Transporter> = new Map();

/**
* Allows sending emails using nodemailer default config or AWS SES
* https://www.nodemailer.com/transports/ses/
*/
export const sendEmail = (smptConfig: SMTPConfig, email: Email) => {
const configStr = JSON.stringify(smptConfig);
const transporter = transporterCache.get(configStr) ?? getTransporter(smptConfig);
if(!transporterCache.has(configStr)){
transporterCache.set(configStr, transporter);
}

return send(transporter, email);
}

const getTransporter = (smptConfig: SMTPConfig) => {
let transporter: Transporter | undefined;
if(smptConfig.type === "aws-ses"){
const {
region,
accessKeyId,
secretAccessKey,
/**
* max 1 messages/second
*/
sendingRate = 1
} = smptConfig;
const ses = new aws.SES({
apiVersion: "2010-12-01",
region,
credentials: {
accessKeyId,
secretAccessKey
}
});

transporter = nodemailer.createTransport({
SES: { ses, aws },
maxConnections: 1,
sendingRate
});

} else {
const { user, pass, host, port, secure } = smptConfig;
transporter = nodemailer.createTransport({
host,
port,
secure,
auth: { user, pass }
});
}

return transporter;
}

const send = (transporter: Transporter, email: Email) => {
return new Promise((resolve, reject) => {
transporter.once('idle', () => {
if (transporter.isIdle()) {
transporter.sendMail(
email,
(err, info) => {
if(err){
reject(err);
} else {
resolve(info);
}
}
);
}
});
});
};
105 changes: 41 additions & 64 deletions lib/Auth/setAuthProviders.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Auth } from './AuthTypes';
/** For some reason normal import is undefined */
const passport = require("passport") as typeof import("passport");
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as GitHubStrategy } from "passport-github2";
import { Strategy as MicrosoftStrategy } from "passport-microsoft";
import { Strategy as FacebookStrategy } from "passport-facebook";
import { AuthSocketSchema, getKeys, isDefined, isEmpty } from "prostgles-types";
import { AUTH_ROUTES_AND_PARAMS, AuthHandler, getLoginClientInfo } from "./AuthHandler";
import type e from "express";
import { RequestHandler } from "express";
import { removeExpressRouteByName } from "../FileManager/FileManager";
import { Strategy as FacebookStrategy } from "passport-facebook";
import { Strategy as GitHubStrategy } from "passport-github2";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as MicrosoftStrategy } from "passport-microsoft";
import { AuthSocketSchema, getObjectEntries, isEmpty } from "prostgles-types";
import { getErrorAsObject } from "../DboBuilder/dboBuilderUtils";

import { removeExpressRouteByName } from "../FileManager/FileManager";
import { AUTH_ROUTES_AND_PARAMS, AuthHandler, getLoginClientInfo } from "./AuthHandler";
import { Auth } from './AuthTypes';
import { setEmailProvider } from "./setEmailProvider";
/** For some reason normal import is undefined */
const passport = require("passport") as typeof import("passport");

export const upsertNamedExpressMiddleware = (app: e.Express, handler: RequestHandler, name: string) => {
const funcName = name;
Expand All @@ -22,56 +22,36 @@ export const upsertNamedExpressMiddleware = (app: e.Express, handler: RequestHan

export function setAuthProviders (this: AuthHandler, { registrations, app }: Required<Auth>["expressConfig"]) {
if(!registrations) return;
const { email, onRegister, onProviderLoginFail, onProviderLoginStart, websiteUrl, ...providers } = registrations;
if(email){
app.post(AUTH_ROUTES_AND_PARAMS.emailSignup, async (req, res) => {
const { username, password } = req.body;
if(typeof username !== "string" || typeof password !== "string"){
res.status(400).json({ msg: "Invalid username or password" });
return;
}
await onRegister({ provider: "email", profile: { username, password }});
})
}
const { onRegister, onProviderLoginFail, onProviderLoginStart, websiteUrl, OAuthProviders } = registrations;

setEmailProvider.bind(this)(app);

if(!isEmpty(providers)){
upsertNamedExpressMiddleware(app, passport.initialize(), "prostglesPassportMiddleware");
if(!OAuthProviders || isEmpty(OAuthProviders)){
return;
}

([
providers.google && {
providerName: "google" as const,
config: providers.google,
strategy: GoogleStrategy,
},
providers.github && {
providerName: "github" as const,
config: providers.github,
strategy: GitHubStrategy,
},
providers.facebook && {
providerName: "facebook" as const,
config: providers.facebook,
strategy: FacebookStrategy,
},
providers.microsoft && {
providerName: "microsoft" as const,
config: providers.microsoft,
strategy: MicrosoftStrategy,
upsertNamedExpressMiddleware(app, passport.initialize(), "prostglesPassportMiddleware");

getObjectEntries(OAuthProviders).forEach(([providerName, providerConfig]) => {

if(!providerConfig?.clientID){
return;
}
])
.filter(isDefined)
.forEach(({
config: { authOpts, ...config },
strategy,
providerName,
}) => {

const { authOpts, ...config } = providerConfig;

const strategy = providerName === "google" ? GoogleStrategy :
providerName === "github" ? GitHubStrategy :
providerName === "facebook" ? FacebookStrategy :
providerName === "microsoft" ? MicrosoftStrategy :
undefined
;

const callbackPath = `${AUTH_ROUTES_AND_PARAMS.loginWithProvider}/${providerName}/callback`;
passport.use(
new (strategy as typeof GoogleStrategy)(
{
...config as any,
...config,
callbackURL: `${websiteUrl}${callbackPath}`,
},
async (accessToken, refreshToken, profile, done) => {
Expand All @@ -92,8 +72,9 @@ export function setAuthProviders (this: AuthHandler, { registrations, app }: Req
try {
const clientInfo = getLoginClientInfo({ httpReq: req });
const db = this.db;
const dbo = this.dbo as any
const startCheck = await onProviderLoginStart({ provider: providerName, req, res, clientInfo, db, dbo });
const dbo = this.dbo as any;
const args = { provider: providerName, req, res, clientInfo, db, dbo };
const startCheck = await onProviderLoginStart(args);
if("error" in startCheck){
res.status(500).json({ error: startCheck.error });
return;
Expand All @@ -107,7 +88,7 @@ export function setAuthProviders (this: AuthHandler, { registrations, app }: Req
},
async (error: any, _profile: any, authInfo: any) => {
if(error){
await onProviderLoginFail({ provider: providerName, error, req, res, clientInfo, db, dbo });
await onProviderLoginFail({ ...args, error });
res.status(500).json({
error: "Failed to login with provider",
});
Expand All @@ -120,7 +101,7 @@ export function setAuthProviders (this: AuthHandler, { registrations, app }: Req
}
)(req, res);

} catch (e) {
} catch (_e) {
res.status(500).json({ error: "Something went wrong" });
}
}
Expand All @@ -132,16 +113,12 @@ export function setAuthProviders (this: AuthHandler, { registrations, app }: Req
export function getProviders(this: AuthHandler): AuthSocketSchema["providers"] | undefined {
const { registrations } = this.opts?.expressConfig ?? {}
if(!registrations) return undefined;
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
email, websiteUrl, onRegister, onProviderLoginFail, onProviderLoginStart,
...providers
} = registrations;
if(isEmpty(providers)) return undefined;
const { OAuthProviders } = registrations;
if(!OAuthProviders || isEmpty(OAuthProviders)) return undefined;

const result: AuthSocketSchema["providers"] = {}
getKeys(providers).forEach(providerName => {
if(providers[providerName]?.clientID){
getObjectEntries(OAuthProviders).forEach(([providerName, config]) => {
if(config?.clientID){
result[providerName] = {
url: `${AUTH_ROUTES_AND_PARAMS.loginWithProvider}/${providerName}`,
}
Expand Down
Loading

0 comments on commit f56d70a

Please sign in to comment.