UNPKG

@nodecg/types

Version:

Dynamic broadcast graphics rendered in a browser

355 lines 16 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createMiddleware = createMiddleware; const crypto = __importStar(require("node:crypto")); const path = __importStar(require("node:path")); const internal_util_1 = require("@nodecg/internal-util"); const cookie_parser_1 = __importDefault(require("cookie-parser")); const express_1 = __importDefault(require("express")); const express_session_1 = __importDefault(require("express-session")); const passport_1 = __importDefault(require("passport")); const passport_discord_1 = require("passport-discord"); const passport_local_1 = require("passport-local"); const passport_steam_1 = __importDefault(require("passport-steam")); const passport_twitch_helix_1 = require("passport-twitch-helix"); const config_1 = require("../config"); const logger_1 = require("../logger"); const log = (0, logger_1.createLogger)("login"); const protocol = config_1.config.ssl?.enabled || (config_1.config.login.enabled && config_1.config.login.forceHttpsReturn) ? "https" : "http"; async function makeDiscordAPIRequest(guild, userID) { const res = await fetch(`https://discord.com/api/v8/guilds/${guild.guildID}/members/${userID}`, { headers: { Authorization: `Bot ${guild.guildBotToken}`, }, }); const data = (await res.json()); if (res.status === 200) { return [guild, false, data]; } return [guild, true, data]; } function createMiddleware(db, callbacks) { // Required for persistent login sessions. // Passport needs ability to serialize and unserialize users out of session. passport_1.default.serializeUser((user, done) => { done(null, user.id); }); passport_1.default.deserializeUser(async (id, done) => { try { done(null, await db.findUser(id)); } catch (error) { done(error); } }); if (config_1.config.login.enabled && config_1.config.login.steam?.enabled && config_1.config.login.steam.apiKey) { const steamLoginConfig = config_1.config.login.steam; const apiKey = config_1.config.login.steam.apiKey; passport_1.default.use(new passport_steam_1.default({ returnURL: `${protocol}://${config_1.config.baseURL}/login/auth/steam`, realm: `${protocol}://${config_1.config.baseURL}/login/auth/steam`, apiKey, }, async (_, profile, done) => { try { const roles = []; const allowed = steamLoginConfig?.allowedIds?.includes(profile.id); if (allowed) { log.info('(Steam) Granting "%s" (%s) access', profile.id, profile.displayName); roles.push(await db.getSuperUserRole()); } else { log.info('(Steam) Denying "%s" (%s) access', profile.id, profile.displayName); } const user = await db.upsertUser({ name: profile.displayName, provider_type: "steam", provider_hash: profile.id, roles, }); done(undefined, user); return; } catch (error) { done(error); } })); } if (config_1.config.login.enabled && config_1.config.login.twitch?.enabled) { const twitchLoginConfig = config_1.config.login.twitch; // The "user:read:email" scope is required. Add it if not present. const scopesArray = twitchLoginConfig.scope.split(" "); if (!scopesArray.includes("user:read:email")) { scopesArray.push("user:read:email"); } const concatScopes = scopesArray.join(" "); passport_1.default.use(new passport_twitch_helix_1.Strategy({ clientID: twitchLoginConfig.clientID, clientSecret: twitchLoginConfig.clientSecret, callbackURL: `${protocol}://${config_1.config.baseURL}/login/auth/twitch`, scope: concatScopes, customHeaders: { "Client-ID": twitchLoginConfig.clientID }, }, async (accessToken, refreshToken, profile, done) => { try { const roles = []; const allowed = twitchLoginConfig.allowedUsernames?.includes(profile.username) ?? twitchLoginConfig.allowedIds?.includes(profile.id); if (allowed) { log.info("(Twitch) Granting %s access", profile.username); roles.push(await db.getSuperUserRole()); } else { log.info("(Twitch) Denying %s access", profile.username); } const user = await db.upsertUser({ name: profile.displayName, provider_type: "twitch", provider_hash: profile.id, provider_access_token: accessToken, provider_refresh_token: refreshToken, roles, }); done(undefined, user); return; } catch (error) { done(error); } })); } if (config_1.config.login.enabled && config_1.config.login.discord?.enabled) { const discordLoginConfig = config_1.config.login.discord; // The "identify" scope is required. Add it if not present. const scopeArray = discordLoginConfig.scope.split(" "); if (!scopeArray.includes("identify")) { scopeArray.push("identify"); } // The "guilds" scope is required if allowedGuilds are used. Add it if not present. if (!scopeArray.includes("guilds") && discordLoginConfig.allowedGuilds) { scopeArray.push("guilds"); } const scope = scopeArray.join(" "); passport_1.default.use(new passport_discord_1.Strategy({ clientID: discordLoginConfig.clientID, clientSecret: discordLoginConfig.clientSecret, callbackURL: `${protocol}://${config_1.config.baseURL}/login/auth/discord`, scope, }, async (accessToken, refreshToken, profile, done) => { if (!discordLoginConfig) { // Impossible but TS doesn't know that. done(new Error("Discord login config was impossibly undefined.")); return; } let allowed = false; if (discordLoginConfig.allowedUserIDs?.includes(profile.id)) { // Users that are on allowedUserIDs are allowed allowed = true; } else if (discordLoginConfig.allowedGuilds) { // Get guilds that are specified in the config and that user is in const intersectingGuilds = discordLoginConfig.allowedGuilds.filter((allowedGuild) => profile.guilds?.some((profileGuild) => profileGuild.id === allowedGuild.guildID)); const guildRequests = []; for (const intersectingGuild of intersectingGuilds) { if (!intersectingGuild.allowedRoleIDs || intersectingGuild.allowedRoleIDs.length === 0) { // If the user matches any guilds that only have member and not role requirements we do not need to make requests to the discord API allowed = true; } else { // Queue up all requests to the Discord API to improve speed guildRequests.push(makeDiscordAPIRequest(intersectingGuild, profile.id)); } } if (!allowed) { const guildsData = await Promise.all(guildRequests); for (const [guildWithRoles, err, memberResponse] of guildsData) { if (err) { log.warn(`Got error while trying to get guild ${guildWithRoles.guildID} ` + `(Make sure you're using the correct bot token and guild id): ${JSON.stringify(memberResponse)}`); continue; } const intersectingRoles = guildWithRoles.allowedRoleIDs.filter((allowedRole) => memberResponse.roles.includes(allowedRole)); if (intersectingRoles.length > 0) { allowed = true; break; } } } } else { allowed = false; } const roles = []; if (allowed) { log.info("(Discord) Granting %s#%s (%s) access", profile.username, profile.discriminator, profile.id); roles.push(await db.getSuperUserRole()); } else { log.info("(Discord) Denying %s#%s (%s) access", profile.username, profile.discriminator, profile.id); } const user = await db.upsertUser({ name: `${profile.username}#${profile.discriminator}`, provider_type: "discord", provider_hash: profile.id, provider_access_token: accessToken, provider_refresh_token: refreshToken, roles, }); done(undefined, user); })); } if (config_1.config.login.enabled && config_1.config.login.local?.enabled && config_1.config.login.sessionSecret) { const { sessionSecret, local: { allowedUsers }, } = config_1.config.login; const hashes = crypto.getHashes(); passport_1.default.use(new passport_local_1.Strategy({ usernameField: "username", passwordField: "password", session: false, }, async (username, password, done) => { try { const roles = []; const foundUser = allowedUsers?.find((u) => u.username === username); let allowed = false; if (foundUser) { const match = /^([^:]+):(.+)$/.exec(foundUser.password ?? ""); let expected = foundUser.password; let actual = password; if (match && hashes.includes(match[1])) { expected = match[2]; actual = crypto .createHmac(match[1], sessionSecret) .update(actual, "utf8") .digest("hex"); } if (expected === actual) { allowed = true; roles.push(await db.getSuperUserRole()); } } log.info('(Local) %s "%s" access', allowed ? "Granting" : "Denying", username); const user = await db.upsertUser({ name: username, provider_type: "local", provider_hash: username, roles, }); done(undefined, user); return; } catch (error) { done(error); } })); } const app = (0, express_1.default)(); const redirectPostLogin = (req, res) => { const url = req.session?.returnTo ?? "/dashboard"; delete req.session.returnTo; res.redirect(url); app.emit("login", req.user); if (req.user) callbacks.onLogin(req.user); }; if (!config_1.config.login.enabled || !config_1.config.login.sessionSecret) { throw new Error("no session secret defined, can't salt sessions, not safe, aborting"); } app.use((0, cookie_parser_1.default)(config_1.config.login.sessionSecret)); const sessionMiddleware = (0, express_session_1.default)({ resave: false, saveUninitialized: false, secret: config_1.config.login.sessionSecret, cookie: { path: "/", httpOnly: true, secure: config_1.config.ssl?.enabled, }, }); app.use(sessionMiddleware); app.use(passport_1.default.initialize()); app.use(passport_1.default.session()); app.use("/login", express_1.default.static(path.join(internal_util_1.nodecgPath, "dist/login"))); app.get("/login", (req, res) => { // If the user is already logged in, don't show them the login page again. if (req.user && db.isSuperUser(req.user)) { res.redirect("/dashboard"); } else { res.render(path.join(__dirname, "views/login.tmpl"), { user: req.user, config: config_1.config, }); } }); app.get("/authError", (req, res) => { res.render(path.join(__dirname, "views/authError.tmpl"), { message: req.query["message"], code: req.query["code"], viewUrl: req.query["viewUrl"], }); }); app.get("/login/steam", passport_1.default.authenticate("steam")); app.get("/login/auth/steam", passport_1.default.authenticate("steam", { failureRedirect: "/login" }), redirectPostLogin); app.get("/login/twitch", passport_1.default.authenticate("twitch")); app.get("/login/auth/twitch", passport_1.default.authenticate("twitch", { failureRedirect: "/login" }), redirectPostLogin); app.get("/login/discord", passport_1.default.authenticate("discord")); app.get("/login/auth/discord", passport_1.default.authenticate("discord", { failureRedirect: "/login" }), redirectPostLogin); app.get("/login/local", passport_1.default.authenticate("local")); app.post("/login/local", passport_1.default.authenticate("local", { failureRedirect: "/login" }), redirectPostLogin); app.get("/logout", (req, res) => { app.emit("logout", req.user); req.session?.destroy(() => { res.clearCookie("connect.sid", { path: "/" }); res.clearCookie("io", { path: "/" }); res.clearCookie("socketToken", { secure: req.secure, sameSite: req.secure ? "none" : undefined, }); res.redirect("/login"); }); if (req.user) callbacks.onLogout(req.user); }); return { app, sessionMiddleware }; } //# sourceMappingURL=index.js.map