2019-06-22 18:52:24 +03:00
|
|
|
import express, { Request, Response } from "express";
|
2019-11-02 22:11:26 +02:00
|
|
|
import passport, { Strategy } from "passport";
|
2019-05-26 00:13:42 +03:00
|
|
|
import OAuth2Strategy from "passport-oauth2";
|
2019-11-02 22:11:26 +02:00
|
|
|
import { Strategy as CustomStrategy } from "passport-custom";
|
2019-06-23 19:18:41 +03:00
|
|
|
import { ApiLogins } from "../data/ApiLogins";
|
2019-05-26 00:13:42 +03:00
|
|
|
import pick from "lodash.pick";
|
|
|
|
import https from "https";
|
2019-06-23 19:18:41 +03:00
|
|
|
import { ApiUserInfo } from "../data/ApiUserInfo";
|
|
|
|
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
2019-11-08 00:04:24 +02:00
|
|
|
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
2019-07-22 00:11:24 +03: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;
|
|
|
|
userId: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
namespace Express {
|
|
|
|
// tslint:disable-next-line:no-empty-interface
|
|
|
|
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}`,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
res => {
|
|
|
|
if (res.statusCode !== 200) {
|
|
|
|
reject(new Error(`Discord API error ${res.statusCode}`));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
res.on("data", data => resolve(JSON.parse(data)));
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
request.on("error", err => reject(err));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-06-22 18:52:24 +03:00
|
|
|
export function initAuth(app: express.Express) {
|
2019-05-26 00:13:42 +03:00
|
|
|
app.use(passport.initialize());
|
|
|
|
|
|
|
|
if (!process.env.CLIENT_ID) {
|
|
|
|
throw new Error("Auth: CLIENT ID missing");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!process.env.CLIENT_SECRET) {
|
|
|
|
throw new Error("Auth: CLIENT SECRET missing");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!process.env.OAUTH_CALLBACK_URL) {
|
|
|
|
throw new Error("Auth: OAUTH CALLBACK URL missing");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!process.env.DASHBOARD_URL) {
|
|
|
|
throw new Error("DASHBOARD_URL missing!");
|
|
|
|
}
|
|
|
|
|
|
|
|
passport.serializeUser((user, done) => done(null, user));
|
|
|
|
passport.deserializeUser((user, done) => done(null, user));
|
|
|
|
|
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");
|
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) {
|
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",
|
2019-05-26 00:13:42 +03:00
|
|
|
clientID: process.env.CLIENT_ID,
|
|
|
|
clientSecret: process.env.CLIENT_SECRET,
|
|
|
|
callbackURL: process.env.OAUTH_CALLBACK_URL,
|
|
|
|
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 });
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
app.get("/auth/login", passport.authenticate("oauth2"));
|
|
|
|
app.get(
|
|
|
|
"/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) {
|
|
|
|
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
|
|
|
|
} else {
|
2019-10-10 21:58:00 +03:00
|
|
|
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?error=noAccess`);
|
2019-07-22 00:11:24 +03:00
|
|
|
}
|
2019-05-26 00:13:42 +03:00
|
|
|
},
|
|
|
|
);
|
2019-06-22 18:52:24 +03:00
|
|
|
app.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
|
|
|
}
|
|
|
|
|
2019-06-23 03:40:53 +03:00
|
|
|
res.json({ valid: true });
|
|
|
|
});
|
2019-07-22 00:49:05 +03:00
|
|
|
app.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
2019-07-22 00:11:24 +03:00
|
|
|
await apiLogins.expireApiKey(req.user.apiKey);
|
|
|
|
return ok(res);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-07-22 00:49:05 +03:00
|
|
|
export function apiTokenAuthHandlers() {
|
2019-07-22 00:11:24 +03:00
|
|
|
return [
|
|
|
|
passport.authenticate("api-token", { failWithError: true }),
|
2019-11-02 22:11:26 +02:00
|
|
|
(err, req: Request, res: Response, next) => {
|
2019-07-22 00:11:24 +03:00
|
|
|
return res.json({ error: err.message });
|
|
|
|
},
|
|
|
|
];
|
2019-06-23 03:40:53 +03:00
|
|
|
}
|