zappyzep/backend/src/api/auth.ts

159 lines
5 KiB
TypeScript
Raw Normal View History

import express, { Request, Response } from "express";
import https from "https";
import pick from "lodash.pick";
import passport from "passport";
import { Strategy as CustomStrategy } from "passport-custom";
import OAuth2Strategy from "passport-oauth2";
2019-06-23 19:18:41 +03:00
import { ApiLogins } from "../data/ApiLogins";
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
2019-06-23 19:18:41 +03:00
import { ApiUserInfo } from "../data/ApiUserInfo";
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
import { env } from "../env";
Update djs & knub (#395) * update pkgs Signed-off-by: GitHub <noreply@github.com> * new knub typings Signed-off-by: GitHub <noreply@github.com> * more pkg updates Signed-off-by: GitHub <noreply@github.com> * more fixes Signed-off-by: GitHub <noreply@github.com> * channel typings Signed-off-by: GitHub <noreply@github.com> * more message utils typings fixes Signed-off-by: GitHub <noreply@github.com> * migrate permissions Signed-off-by: GitHub <noreply@github.com> * fix: InternalPoster webhookables Signed-off-by: GitHub <noreply@github.com> * djs typings: Attachment & Util Signed-off-by: GitHub <noreply@github.com> * more typings Signed-off-by: GitHub <noreply@github.com> * fix: rename permissionNames Signed-off-by: GitHub <noreply@github.com> * more fixes Signed-off-by: GitHub <noreply@github.com> * half the number of errors * knub commands => messageCommands Signed-off-by: GitHub <noreply@github.com> * configPreprocessor => configParser Signed-off-by: GitHub <noreply@github.com> * fix channel.messages Signed-off-by: GitHub <noreply@github.com> * revert automod any typing Signed-off-by: GitHub <noreply@github.com> * more configParser typings Signed-off-by: GitHub <noreply@github.com> * revert Signed-off-by: GitHub <noreply@github.com> * remove knub type params Signed-off-by: GitHub <noreply@github.com> * fix more MessageEmbed / MessageOptions Signed-off-by: GitHub <noreply@github.com> * dumb commit for @almeidx to see why this is stupid Signed-off-by: GitHub <noreply@github.com> * temp disable custom_events Signed-off-by: GitHub <noreply@github.com> * more minor typings fixes - 23 err left Signed-off-by: GitHub <noreply@github.com> * update djs dep * +debug build method (revert this) Signed-off-by: GitHub <noreply@github.com> * Revert "+debug build method (revert this)" This reverts commit a80af1e729b742d1aad1097df538d224fbd32ce7. * Redo +debug build (Revert this) Signed-off-by: GitHub <noreply@github.com> * uniform before/after Load shorthands Signed-off-by: GitHub <noreply@github.com> * remove unused imports & add prettier plugin Signed-off-by: GitHub <noreply@github.com> * env fixes for web platform hosting Signed-off-by: GitHub <noreply@github.com> * feat: knub v32-next; related fixes * fix: allow legacy keys in change_perms action * fix: request Message Content intent * fix: use Knub's config validation logic in API * fix(dashboard): fix error when there are no message and/or slash commands in a plugin * fix(automod): start_thread action thread options * fix(CustomEvents): message command types * chore: remove unneeded type annotation * feat: add forum channel icon; use thread icon for news threads * chore: make tslint happy * chore: fix formatting --------- Signed-off-by: GitHub <noreply@github.com> Co-authored-by: almeidx <almeidx@pm.me> Co-authored-by: Dragory <2606411+Dragory@users.noreply.github.com>
2023-04-01 12:58:17 +01:00
import { ok } from "./responses";
2019-05-26 00:13:42 +03:00
interface IPassportApiUser {
apiKey: string;
userId: string;
}
declare global {
namespace Express {
interface User extends IPassportApiUser {}
}
}
2020-05-04 21:56:15 +03:00
const DISCORD_API_URL = "https://discord.com/api";
2019-05-26 00:13:42 +03:00
function simpleDiscordAPIRequest(bearerToken, path): Promise<any> {
2019-05-26 00:13:42 +03:00
return new Promise((resolve, reject) => {
const request = https.get(
`${DISCORD_API_URL}/${path}`,
{
headers: {
Authorization: `Bearer ${bearerToken}`,
},
},
2021-09-11 19:06:51 +03:00
(res) => {
2019-05-26 00:13:42 +03:00
if (res.statusCode !== 200) {
reject(new Error(`Discord API error ${res.statusCode}`));
return;
}
let rawData = "";
2021-09-11 19:06:51 +03:00
res.on("data", (data) => (rawData += data));
res.on("end", () => {
resolve(JSON.parse(rawData));
});
2019-05-26 00:13:42 +03:00
},
);
2021-09-11 19:06:51 +03:00
request.on("error", (err) => reject(err));
2019-05-26 00:13:42 +03:00
});
}
2024-04-06 11:54:31 +00:00
export function initAuth(router: express.Router) {
router.use(passport.initialize());
2019-05-26 00:13:42 +03:00
passport.serializeUser((user, done) => done(null, user));
2023-04-01 20:31:44 +03:00
passport.deserializeUser((user, done) => done(null, user as IPassportApiUser));
2019-05-26 00:13:42 +03:00
2019-06-23 19:18:41 +03:00
const apiLogins = new ApiLogins();
const apiUserInfo = new ApiUserInfo();
2019-11-08 00:04:24 +02:00
const apiPermissionAssignments = new ApiPermissionAssignments();
2019-05-26 00:13:42 +03:00
// Initialize API tokens
passport.use(
"api-token",
new CustomStrategy(async (req, cb) => {
const apiKey = req.header("X-Api-Key") || req.body?.["X-Api-Key"];
if (!apiKey) return cb("API key missing");
2019-05-26 00:13:42 +03:00
2019-06-23 19:18:41 +03:00
const userId = await apiLogins.getUserIdByApiKey(apiKey);
2019-05-26 00:13:42 +03:00
if (userId) {
void apiLogins.refreshApiKeyExpiryTime(apiKey); // Refresh expiry time in the background
return cb(null, { apiKey, userId });
2019-05-26 00:13:42 +03:00
}
cb("API key not found");
2019-05-26 00:13:42 +03:00
}),
);
// Initialize OAuth2 for Discord login
2019-06-23 19:18:41 +03:00
// When the user logs in through OAuth2, we create them a "login" (= api token) and update their user info in the DB
2019-05-26 00:13:42 +03:00
passport.use(
new OAuth2Strategy(
{
2020-05-04 21:56:15 +03:00
authorizationURL: "https://discord.com/api/oauth2/authorize",
tokenURL: "https://discord.com/api/oauth2/token",
clientID: env.CLIENT_ID,
clientSecret: env.CLIENT_SECRET,
2022-06-26 19:30:46 +03:00
callbackURL: `${env.API_URL}/auth/oauth-callback`,
2019-05-26 00:13:42 +03:00
scope: ["identify"],
},
async (accessToken, refreshToken, profile, cb) => {
const user = await simpleDiscordAPIRequest(accessToken, "users/@me");
// Make sure the user is able to access at least 1 guild
2019-11-08 00:04:24 +02:00
const permissions = await apiPermissionAssignments.getByUserId(user.id);
if (permissions.length === 0) {
cb(null, {});
return;
}
// Generate API key
2019-06-23 19:18:41 +03:00
const apiKey = await apiLogins.addLogin(user.id);
const userData = pick(user, ["username", "discriminator", "avatar"]) as ApiUserInfoData;
await apiUserInfo.update(user.id, userData);
2019-05-26 00:13:42 +03:00
// TODO: Revoke access token, we don't need it anymore
cb(null, { apiKey });
},
),
);
2024-04-06 11:54:31 +00:00
router.get("/auth/login", passport.authenticate("oauth2"));
router.get(
2019-05-26 00:13:42 +03:00
"/auth/oauth-callback",
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
(req: Request, res: Response) => {
if (req.user && req.user.apiKey) {
2022-06-26 19:30:46 +03:00
res.redirect(`${env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
} else {
2022-06-26 19:30:46 +03:00
res.redirect(`${env.DASHBOARD_URL}/login-callback/?error=noAccess`);
}
2019-05-26 00:13:42 +03:00
},
);
2024-04-06 11:54:31 +00:00
router.post("/auth/validate-key", async (req: Request, res: Response) => {
const key = req.body.key;
if (!key) {
return res.status(400).json({ error: "No key supplied" });
}
2019-06-23 19:18:41 +03:00
const userId = await apiLogins.getUserIdByApiKey(key);
if (!userId) {
return res.json({ valid: false });
}
res.json({ valid: true, userId });
});
2024-04-06 11:54:31 +00:00
router.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
await apiLogins.expireApiKey(req.user!.apiKey);
return ok(res);
});
// API route to refresh the given API token's expiry time
// The actual refreshing happens in the api-token passport strategy above, so we just return 200 OK here
2024-04-06 11:54:31 +00:00
router.post("/auth/refresh", ...apiTokenAuthHandlers(), (req, res) => {
return ok(res);
});
}
export function apiTokenAuthHandlers() {
return [
passport.authenticate("api-token", { failWithError: true, session: false }),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(err, req: Request, res: Response, next) => {
return res.status(401).json({ error: err.message });
},
];
}