diff --git a/dashboard/src/auth.ts b/dashboard/src/auth.ts
index 4b934970..6795f4b4 100644
--- a/dashboard/src/auth.ts
+++ b/dashboard/src/auth.ts
@@ -15,8 +15,17 @@ export const authGuard: NavigationGuard = async (to, from, next) => {
};
export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
- await RootStore.dispatch("auth/setApiKey", to.query.apiKey);
- next("/dashboard");
+ if (to.query.apiKey) {
+ await RootStore.dispatch("auth/setApiKey", to.query.apiKey);
+ next("/dashboard");
+ } else {
+ next({
+ path: "/",
+ query: {
+ error: "noaccess",
+ },
+ });
+ }
};
export const authRedirectGuard: NavigationGuard = async (to, form, next) => {
diff --git a/dashboard/src/components/Dashboard.vue b/dashboard/src/components/Dashboard.vue
index e610668c..084c135b 100644
--- a/dashboard/src/components/Dashboard.vue
+++ b/dashboard/src/components/Dashboard.vue
@@ -13,7 +13,7 @@
@@ -41,6 +41,12 @@
export default {
async mounted() {
await import("../style/dashboard.scss");
- }
+ },
+ methods: {
+ async logout() {
+ await this.$store.dispatch("auth/logout");
+ this.$router.push('/');
+ }
+ },
};
diff --git a/dashboard/src/components/Splash.vue b/dashboard/src/components/Splash.vue
index 51a07a78..165e2e27 100644
--- a/dashboard/src/components/Splash.vue
+++ b/dashboard/src/components/Splash.vue
@@ -1,14 +1,22 @@
-

-
Zeppelin
-
- Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind.
+
+
-
-
Dashboard
-
Docs
+
+
Zeppelin
+
+ Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind.
+
+
+
@@ -16,4 +24,12 @@
diff --git a/dashboard/src/store/auth.ts b/dashboard/src/store/auth.ts
index dd00d335..5b08a953 100644
--- a/dashboard/src/store/auth.ts
+++ b/dashboard/src/store/auth.ts
@@ -35,6 +35,16 @@ export const AuthStore: Module
= {
localStorage.setItem("apiKey", newKey);
commit("setApiKey", newKey);
},
+
+ clearApiKey({ commit }) {
+ localStorage.removeItem("apiKey");
+ commit("setApiKey", null);
+ },
+
+ async logout({ dispatch }) {
+ await post("auth/logout");
+ await dispatch("clearApiKey");
+ },
},
mutations: {
diff --git a/dashboard/src/style/splash.scss b/dashboard/src/style/splash.scss
index 0d5cc145..b2a278de 100644
--- a/dashboard/src/style/splash.scss
+++ b/dashboard/src/style/splash.scss
@@ -7,46 +7,50 @@
color: #fff;
display: flex;
- flex-direction: column;
- align-items: center;
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-start;
a {
color: #fff;
}
.wrapper {
- display: grid;
- grid-template-columns: auto 400px;
- grid-template-rows: auto repeat(4, 1fr);
+ flex: 0 1 750px;
+
+ display: flex;
+ flex-direction: row;
align-items: start;
- .logo {
- grid-column: 1;
- grid-row: 1/-1; // Span all
+ .logo-column {
+ flex: 0 0 auto;
+ }
+ .info-column {
+ flex: 1 1 100%;
+ }
+
+ .logo {
width: 300px;
height: 300px;
margin-right: 64px;
}
h1 {
- grid-column: 2;
-
font-size: 80px;
font-weight: 300;
margin-top: 40px
}
.description {
- grid-column: 2;
-
color: #f1f5ff;
}
.actions {
- grid-column: 2;
-
display: flex;
+ flex-wrap: wrap;
+ margin-top: 8px;
+ margin-left: -12px; // Negative button margin
.btn {
margin: 12px;
@@ -68,5 +72,12 @@
}
}
}
+
+ .error {
+ margin-top: 8px;
+ background-color: hsl(224, 52%, 32%);
+ padding: 12px;
+ border-radius: 4px;
+ }
}
}
diff --git a/src/api/auth.ts b/src/api/auth.ts
index b4231462..2795ae78 100644
--- a/src/api/auth.ts
+++ b/src/api/auth.ts
@@ -7,6 +7,8 @@ import pick from "lodash.pick";
import https from "https";
import { ApiUserInfo } from "../data/ApiUserInfo";
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
+import { ApiPermissions } from "../data/ApiPermissions";
+import { ok } from "./responses";
const DISCORD_API_URL = "https://discordapp.com/api";
@@ -57,6 +59,7 @@ export function initAuth(app: express.Express) {
const apiLogins = new ApiLogins();
const apiUserInfo = new ApiUserInfo();
+ const apiPermissions = new ApiPermissions();
// Initialize API tokens
passport.use(
@@ -67,7 +70,7 @@ export function initAuth(app: express.Express) {
const userId = await apiLogins.getUserIdByApiKey(apiKey);
if (userId) {
- return cb(null, { userId });
+ return cb(null, { apiKey, userId });
}
cb();
@@ -88,6 +91,15 @@ export function initAuth(app: express.Express) {
},
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
const apiKey = await apiLogins.addLogin(user.id);
const userData = pick(user, ["username", "discriminator", "avatar"]) as ApiUserInfoData;
await apiUserInfo.update(user.id, userData);
@@ -102,12 +114,15 @@ export function initAuth(app: express.Express) {
"/auth/oauth-callback",
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
(req, res) => {
- console.log("redirecting to a non-existent page haHAA");
- res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
+ 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`);
+ }
},
);
app.post("/auth/validate-key", async (req: Request, res: Response) => {
- const key = req.params.key || req.query.key;
+ const key = req.body.key;
if (!key) {
return res.status(400).json({ error: "No key supplied" });
}
@@ -119,10 +134,21 @@ export function initAuth(app: express.Express) {
res.json({ valid: true });
});
+ app.post("/auth/logout", ...getRequireAPITokenHandlers(), async (req: Request, res: Response) => {
+ await apiLogins.expireApiKey(req.user.apiKey);
+ return ok(res);
+ });
+}
+
+function getRequireAPITokenHandlers() {
+ return [
+ passport.authenticate("api-token", { failWithError: true }),
+ (err, req, res, next) => {
+ return res.json({ error: err.message });
+ },
+ ];
}
export function requireAPIToken(router: express.Router) {
- router.use(passport.authenticate("api-token", { failWithError: true }), (err, req, res, next) => {
- return res.json({ error: err.message });
- });
+ router.use(...getRequireAPITokenHandlers());
}
diff --git a/src/api/index.ts b/src/api/index.ts
index 29da96ed..3a987267 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1,7 +1,4 @@
import { error, notFound } from "./responses";
-
-require("dotenv").config({ path: path.resolve(__dirname, "..", "..", "api.env") });
-
import express from "express";
import cors from "cors";
import { initAuth } from "./auth";
@@ -10,6 +7,8 @@ import { initArchives } from "./archives";
import { connect } from "../data/db";
import path from "path";
+require("dotenv").config({ path: path.resolve(__dirname, "..", "..", "api.env") });
+
console.log("Connecting to database...");
connect().then(() => {
const app = express();
diff --git a/src/data/ApiLogins.ts b/src/data/ApiLogins.ts
index f9c2edfa..2b775cdf 100644
--- a/src/data/ApiLogins.ts
+++ b/src/data/ApiLogins.ts
@@ -75,4 +75,16 @@ export class ApiLogins extends BaseRepository {
return `${loginId}.${token}`;
}
+
+ expireApiKey(apiKey) {
+ const [loginId, token] = apiKey.split(".");
+ if (!loginId || !token) return;
+
+ return this.apiLogins.update(
+ { id: loginId },
+ {
+ expires_at: moment().format(DBDateFormat),
+ },
+ );
+ }
}
diff --git a/src/data/ApiPermissions.ts b/src/data/ApiPermissions.ts
index f956ab77..7fe05d9a 100644
--- a/src/data/ApiPermissions.ts
+++ b/src/data/ApiPermissions.ts
@@ -10,6 +10,14 @@ export class ApiPermissions extends BaseRepository {
this.apiPermissions = getRepository(ApiPermission);
}
+ getByUserId(userId) {
+ return this.apiPermissions.find({
+ where: {
+ user_id: userId,
+ },
+ });
+ }
+
getByGuildAndUserId(guildId, userId) {
return this.apiPermissions.findOne({
where: {