2019-06-22 18:52:24 +03:00
|
|
|
import express, { Request, Response } from "express";
|
2021-06-06 23:51:32 +02:00
|
|
|
import https from "https";
|
|
|
|
import pick from "lodash.pick";
|
2020-07-22 22:56:21 +03:00
|
|
|
import passport from "passport";
|
2019-11-02 22:11:26 +02:00
|
|
|
import { Strategy as CustomStrategy } from "passport-custom";
|
2021-06-06 23:51:32 +02:00
|
|
|
import OAuth2Strategy from "passport-oauth2";
|
2019-06-23 19:18:41 +03:00
|
|
|
import { ApiLogins } from "../data/ApiLogins";
|
2021-06-06 23:51:32 +02:00
|
|
|
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
2019-06-23 19:18:41 +03:00
|
|
|
import { ApiUserInfo } from "../data/ApiUserInfo";
|
|
|
|
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
2022-06-26 14:34:54 +03:00
|
|
|
import { env } from "../env";
|
2023-04-01 12:58:17 +01:00
|
|
|
import { ok } from "./responses";
|
2019-05-26 00:13:42 +03:00
|
|
|
|
2019-11-02 22:11:26 +02:00
|
|
|
interface IPassportApiUser {
|
|
|
|
apiKey: string;
|
2020-05-23 16:22:03 +03:00
|
|
|
userId: string;
|
2019-11-02 22:11:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
2019-06-22 18:52:24 +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;
|
|
|
|
}
|
|
|
|
|
2021-04-12 13:04:04 +03:00
|
|
|
let rawData = "";
|
2021-09-11 19:06:51 +03:00
|
|
|
res.on("data", (data) => (rawData += data));
|
2021-04-12 13:04:04 +03:00
|
|
|
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) => {
|
2021-11-03 01:14:41 +02:00
|
|
|
const apiKey = req.header("X-Api-Key") || req.body?.["X-Api-Key"];
|
2019-11-02 22:11:26 +02:00
|
|
|
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) {
|
2021-05-22 21:15:13 +03:00
|
|
|
void apiLogins.refreshApiKeyExpiryTime(apiKey); // Refresh expiry time in the background
|
2019-07-22 00:11:24 +03:00
|
|
|
return cb(null, { apiKey, userId });
|
2019-05-26 00:13:42 +03:00
|
|
|
}
|
|
|
|
|
2019-11-02 22:11:26 +02: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",
|
2022-06-26 14:34:54 +03:00
|
|
|
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) => {
|
2019-06-22 18:52:24 +03:00
|
|
|
const user = await simpleDiscordAPIRequest(accessToken, "users/@me");
|
2019-07-22 00:11:24 +03:00
|
|
|
|
|
|
|
// 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);
|
2019-07-22 00:11:24 +03:00
|
|
|
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 }),
|
2019-11-02 22:11:26 +02:00
|
|
|
(req: Request, res: Response) => {
|
2019-07-22 00:11:24 +03:00
|
|
|
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}`);
|
2019-07-22 00:11:24 +03:00
|
|
|
} else {
|
2022-06-26 19:30:46 +03:00
|
|
|
res.redirect(`${env.DASHBOARD_URL}/login-callback/?error=noAccess`);
|
2019-07-22 00:11:24 +03:00
|
|
|
}
|
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) => {
|
2019-07-22 00:11:24 +03:00
|
|
|
const key = req.body.key;
|
2019-06-22 18:52:24 +03:00
|
|
|
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);
|
2019-06-22 18:52:24 +03:00
|
|
|
if (!userId) {
|
2019-06-23 03:40:53 +03:00
|
|
|
return res.json({ valid: false });
|
2019-06-22 18:52:24 +03:00
|
|
|
}
|
|
|
|
|
2021-09-05 16:42:35 +03:00
|
|
|
res.json({ valid: true, userId });
|
2019-06-23 03:40:53 +03:00
|
|
|
});
|
2024-04-06 11:54:31 +00:00
|
|
|
router.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
2020-11-09 20:03:57 +02:00
|
|
|
await apiLogins.expireApiKey(req.user!.apiKey);
|
2019-07-22 00:11:24 +03:00
|
|
|
return ok(res);
|
|
|
|
});
|
2021-05-22 21:15:13 +03:00
|
|
|
|
|
|
|
// 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) => {
|
2021-05-22 21:15:13 +03:00
|
|
|
return ok(res);
|
|
|
|
});
|
2019-07-22 00:11:24 +03:00
|
|
|
}
|
|
|
|
|
2019-07-22 00:49:05 +03:00
|
|
|
export function apiTokenAuthHandlers() {
|
2019-07-22 00:11:24 +03:00
|
|
|
return [
|
2023-07-01 17:09:23 +00:00
|
|
|
passport.authenticate("api-token", { failWithError: true, session: false }),
|
2023-05-08 22:58:51 +03:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2019-11-02 22:11:26 +02:00
|
|
|
(err, req: Request, res: Response, next) => {
|
2021-05-22 21:33:34 +03:00
|
|
|
return res.status(401).json({ error: err.message });
|
2019-07-22 00:11:24 +03:00
|
|
|
},
|
|
|
|
];
|
2019-06-23 03:40:53 +03:00
|
|
|
}
|