import type { Express } from "express"; import invariant from "tiny-invariant"; import session from "express-session"; import cookieParser from "cookie-parser"; import passport from "passport"; import { Strategy as DiscordStrategy } from "passport-discord"; import { db } from "../app/utils/db.server"; declare module "express-session" { export interface SessionData { returnTo?: string; } } export function setUpAuth(app: Express): void { invariant( process.env.DISCORD_CLIENT_ID, "env var DISCORD_CLIENT_ID undefined" ); invariant( process.env.DISCORD_CLIENT_SECRET, "env var DISCORD_CLIENT_SECRET undefined" ); invariant(process.env.COOKIE_SECRET, "env var COOKIE_SECRET undefined"); passport.use( new DiscordStrategy( { clientID: process.env.DISCORD_CLIENT_ID, clientSecret: process.env.DISCORD_CLIENT_SECRET, callbackURL: `${process.env.FRONT_PAGE_URL}auth/discord/callback`, scope: ["identify", "connections"], }, function (_accessToken, refreshToken, loggedInUser, cb) { db.user .upsert({ create: { discordId: loggedInUser.id, discordName: loggedInUser.username, discordDiscriminator: loggedInUser.discriminator, discordAvatar: loggedInUser.avatar, discordRefreshToken: refreshToken, ...parseConnections(), }, update: { discordName: loggedInUser.username, discordDiscriminator: loggedInUser.discriminator, discordAvatar: loggedInUser.avatar, discordRefreshToken: refreshToken, ...parseConnections(), }, where: { discordId: loggedInUser.id, }, }) .then((user) => { return cb(null, { id: user.id, discordId: user.discordId, discordAvatar: user.discordAvatar, }); }) .catch((err: unknown) => { if (err instanceof Error || err === undefined || err === null) { cb(err); } }); function parseConnections() { if (!loggedInUser.connections) return null; const result: { twitch?: string; twitter?: string; youtubeId?: string; youtubeName?: string; } = {}; for (const connection of loggedInUser.connections) { if (connection.visibility !== 1 || !connection.verified) continue; switch (connection.type) { case "twitch": result.twitch = connection.name; break; case "twitter": result.twitter = connection.name; break; case "youtube": result.youtubeId = connection.id; result.youtubeName = connection.name; } } return result; } } ) ); passport.serializeUser(function (user, done) { done(null, user); }); passport.deserializeUser(function (user, done) { // @ts-expect-error it is guaranteed it's of a certain shape without an extra check done(null, user); }); app.use(cookieParser()); app.use( session({ secret: process.env.COOKIE_SECRET, resave: true, saveUninitialized: true, }) ); app.use(passport.initialize()); app.use(passport.session()); app.post("/auth/discord", (req, res) => { const returnTo = req.query.origin ?? req.header("Referer"); if (returnTo) { invariant(typeof returnTo === "string", "returnTo is not string"); req.session.returnTo = returnTo; } // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call return passport.authenticate("discord")(req, res); }); app.get("/auth/discord/callback", (req, res) => { const returnTo = req.session.returnTo ?? process.env.FRONT_PAGE_URL; if (req.session.returnTo) { delete req.session.returnTo; } // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call return passport.authenticate("discord", { failureRedirect: "/login", successRedirect: returnTo, })(req, res); }); app.use(function (req, _res, next) { if (req.session.returnTo) { delete req.session.returnTo; } next(); }); }