Dashboard styling; don't allow login if you have no guild perms; allow logging out
This commit is contained in:
parent
a517ca3906
commit
0f724fc9bd
9 changed files with 132 additions and 35 deletions
|
@ -15,8 +15,17 @@ export const authGuard: NavigationGuard = async (to, from, next) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
|
export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
|
||||||
await RootStore.dispatch("auth/setApiKey", to.query.apiKey);
|
if (to.query.apiKey) {
|
||||||
next("/dashboard");
|
await RootStore.dispatch("auth/setApiKey", to.query.apiKey);
|
||||||
|
next("/dashboard");
|
||||||
|
} else {
|
||||||
|
next({
|
||||||
|
path: "/",
|
||||||
|
query: {
|
||||||
|
error: "noaccess",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authRedirectGuard: NavigationGuard = async (to, form, next) => {
|
export const authRedirectGuard: NavigationGuard = async (to, form, next) => {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<router-link to="/dashboard" class="navbar-item">Guilds</router-link>
|
<router-link to="/dashboard" class="navbar-item">Guilds</router-link>
|
||||||
<a href="#" class="navbar-item">Docs</a>
|
<a href="#" class="navbar-item">Docs</a>
|
||||||
<a href="#" class="navbar-item">Log out</a>
|
<a href="javascript:void(0)" class="navbar-item" v-on:click="logout()">Log out</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,6 +41,12 @@
|
||||||
export default {
|
export default {
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await import("../style/dashboard.scss");
|
await import("../style/dashboard.scss");
|
||||||
}
|
},
|
||||||
|
methods: {
|
||||||
|
async logout() {
|
||||||
|
await this.$store.dispatch("auth/logout");
|
||||||
|
this.$router.push('/');
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="splash">
|
<div class="splash">
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<img class="logo" src="../img/logo.png" alt="Zeppelin Logo">
|
<div class="logo-column">
|
||||||
<h1>Zeppelin</h1>
|
<img class="logo" src="../img/logo.png" alt="Zeppelin Logo">
|
||||||
<div class="description">
|
|
||||||
Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind.
|
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="info-column">
|
||||||
<a class="btn" href="/login">Dashboard</a>
|
<h1>Zeppelin</h1>
|
||||||
<a class="btn disabled" href="#">Docs</a>
|
<div class="description">
|
||||||
|
Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind.
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="btn" href="/login">Dashboard</a>
|
||||||
|
<a class="btn disabled" href="#">Docs</a>
|
||||||
|
</div>
|
||||||
|
<div class="error" v-if="error">
|
||||||
|
<strong>Error</strong>
|
||||||
|
<div v-if="error === 'noaccess'">No access</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,4 +24,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "../style/splash.scss";
|
import "../style/splash.scss";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
error() {
|
||||||
|
return this.$route.query.error;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -35,6 +35,16 @@ export const AuthStore: Module<AuthState, RootState> = {
|
||||||
localStorage.setItem("apiKey", newKey);
|
localStorage.setItem("apiKey", newKey);
|
||||||
commit("setApiKey", newKey);
|
commit("setApiKey", newKey);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearApiKey({ commit }) {
|
||||||
|
localStorage.removeItem("apiKey");
|
||||||
|
commit("setApiKey", null);
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout({ dispatch }) {
|
||||||
|
await post("auth/logout");
|
||||||
|
await dispatch("clearApiKey");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
|
|
|
@ -7,46 +7,50 @@
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: grid;
|
flex: 0 1 750px;
|
||||||
grid-template-columns: auto 400px;
|
|
||||||
grid-template-rows: auto repeat(4, 1fr);
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
||||||
.logo {
|
.logo-column {
|
||||||
grid-column: 1;
|
flex: 0 0 auto;
|
||||||
grid-row: 1/-1; // Span all
|
}
|
||||||
|
|
||||||
|
.info-column {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
margin-right: 64px;
|
margin-right: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
grid-column: 2;
|
|
||||||
|
|
||||||
font-size: 80px;
|
font-size: 80px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
margin-top: 40px
|
margin-top: 40px
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
grid-column: 2;
|
|
||||||
|
|
||||||
color: #f1f5ff;
|
color: #f1f5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
grid-column: 2;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: -12px; // Negative button margin
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
|
@ -68,5 +72,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-top: 8px;
|
||||||
|
background-color: hsl(224, 52%, 32%);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import pick from "lodash.pick";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import { ApiUserInfo } from "../data/ApiUserInfo";
|
import { ApiUserInfo } from "../data/ApiUserInfo";
|
||||||
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
||||||
|
import { ApiPermissions } from "../data/ApiPermissions";
|
||||||
|
import { ok } from "./responses";
|
||||||
|
|
||||||
const DISCORD_API_URL = "https://discordapp.com/api";
|
const DISCORD_API_URL = "https://discordapp.com/api";
|
||||||
|
|
||||||
|
@ -57,6 +59,7 @@ export function initAuth(app: express.Express) {
|
||||||
|
|
||||||
const apiLogins = new ApiLogins();
|
const apiLogins = new ApiLogins();
|
||||||
const apiUserInfo = new ApiUserInfo();
|
const apiUserInfo = new ApiUserInfo();
|
||||||
|
const apiPermissions = new ApiPermissions();
|
||||||
|
|
||||||
// Initialize API tokens
|
// Initialize API tokens
|
||||||
passport.use(
|
passport.use(
|
||||||
|
@ -67,7 +70,7 @@ export function initAuth(app: express.Express) {
|
||||||
|
|
||||||
const userId = await apiLogins.getUserIdByApiKey(apiKey);
|
const userId = await apiLogins.getUserIdByApiKey(apiKey);
|
||||||
if (userId) {
|
if (userId) {
|
||||||
return cb(null, { userId });
|
return cb(null, { apiKey, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
cb();
|
cb();
|
||||||
|
@ -88,6 +91,15 @@ export function initAuth(app: express.Express) {
|
||||||
},
|
},
|
||||||
async (accessToken, refreshToken, profile, cb) => {
|
async (accessToken, refreshToken, profile, cb) => {
|
||||||
const user = await simpleDiscordAPIRequest(accessToken, "users/@me");
|
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 apiKey = await apiLogins.addLogin(user.id);
|
||||||
const userData = pick(user, ["username", "discriminator", "avatar"]) as ApiUserInfoData;
|
const userData = pick(user, ["username", "discriminator", "avatar"]) as ApiUserInfoData;
|
||||||
await apiUserInfo.update(user.id, userData);
|
await apiUserInfo.update(user.id, userData);
|
||||||
|
@ -102,12 +114,15 @@ export function initAuth(app: express.Express) {
|
||||||
"/auth/oauth-callback",
|
"/auth/oauth-callback",
|
||||||
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
|
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
console.log("redirecting to a non-existent page haHAA");
|
if (req.user && req.user.apiKey) {
|
||||||
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${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) => {
|
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) {
|
if (!key) {
|
||||||
return res.status(400).json({ error: "No key supplied" });
|
return res.status(400).json({ error: "No key supplied" });
|
||||||
}
|
}
|
||||||
|
@ -119,10 +134,21 @@ export function initAuth(app: express.Express) {
|
||||||
|
|
||||||
res.json({ valid: true });
|
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) {
|
export function requireAPIToken(router: express.Router) {
|
||||||
router.use(passport.authenticate("api-token", { failWithError: true }), (err, req, res, next) => {
|
router.use(...getRequireAPITokenHandlers());
|
||||||
return res.json({ error: err.message });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import { error, notFound } from "./responses";
|
import { error, notFound } from "./responses";
|
||||||
|
|
||||||
require("dotenv").config({ path: path.resolve(__dirname, "..", "..", "api.env") });
|
|
||||||
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import { initAuth } from "./auth";
|
import { initAuth } from "./auth";
|
||||||
|
@ -10,6 +7,8 @@ import { initArchives } from "./archives";
|
||||||
import { connect } from "../data/db";
|
import { connect } from "../data/db";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
require("dotenv").config({ path: path.resolve(__dirname, "..", "..", "api.env") });
|
||||||
|
|
||||||
console.log("Connecting to database...");
|
console.log("Connecting to database...");
|
||||||
connect().then(() => {
|
connect().then(() => {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
|
@ -75,4 +75,16 @@ export class ApiLogins extends BaseRepository {
|
||||||
|
|
||||||
return `${loginId}.${token}`;
|
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),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,14 @@ export class ApiPermissions extends BaseRepository {
|
||||||
this.apiPermissions = getRepository(ApiPermission);
|
this.apiPermissions = getRepository(ApiPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getByUserId(userId) {
|
||||||
|
return this.apiPermissions.find({
|
||||||
|
where: {
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getByGuildAndUserId(guildId, userId) {
|
getByGuildAndUserId(guildId, userId) {
|
||||||
return this.apiPermissions.findOne({
|
return this.apiPermissions.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
|
Loading…
Add table
Reference in a new issue