zappyzep/backend/src/api/auth.ts

163 lines
4.8 KiB
TypeScript
Raw Normal View History

import express, { Request, Response } from "express";
import passport, { Strategy } from "passport";
2019-05-26 00:13:42 +03:00
import OAuth2Strategy from "passport-oauth2";
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";
import { ApiPermissions } from "../data/ApiPermissions";
import { ok } from "./responses";
2019-05-26 00:13:42 +03:00
interface IPassportApiUser {
apiKey: string;
userId: number;
}
declare global {
namespace Express {
// tslint:disable-next-line:no-empty-interface
interface User extends IPassportApiUser {}
}
}
2019-05-26 00:13:42 +03:00
const DISCORD_API_URL = "https://discordapp.com/api";
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));
});
}
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();
const apiPermissions = new ApiPermissions();
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");
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) {
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(
{
authorizationURL: "https://discordapp.com/api/oauth2/authorize",
tokenURL: "https://discordapp.com/api/oauth2/token",
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.OAUTH_CALLBACK_URL,
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
const permissions = await apiPermissions.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 });
},
),
);
app.get("/auth/login", passport.authenticate("oauth2"));
app.get(
"/auth/oauth-callback",
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
(req: Request, res: Response) => {
if (req.user && req.user.apiKey) {
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
} else {
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?error=noAccess`);
}
2019-05-26 00:13:42 +03:00
},
);
app.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 });
});
app.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
await apiLogins.expireApiKey(req.user.apiKey);
return ok(res);
});
}
export function apiTokenAuthHandlers() {
return [
passport.authenticate("api-token", { failWithError: true }),
(err, req: Request, res: Response, next) => {
return res.json({ error: err.message });
},
];
}