diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 8a385f4e..84ca0758 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -2193,6 +2193,11 @@ "q": "^1.1.2" } }, + "codemirror": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.48.0.tgz", + "integrity": "sha512-3Ter+tYtRlTNtxtYdYNPxGxBL/b3cMcvPdPm70gvmcOO2Rauv/fUEewWa0tT596Hosv6ea2mtpx28OXBy1mQCg==" + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -2935,6 +2940,11 @@ "repeating": "^2.0.0" } }, + "diff-match-patch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz", + "integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg==" + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -7540,6 +7550,15 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz", "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" }, + "vue-codemirror": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/vue-codemirror/-/vue-codemirror-4.0.6.tgz", + "integrity": "sha512-ilU7Uf0mqBNSSV3KT7FNEeRIxH4s1fmpG4TfHlzvXn0QiQAbkXS9lLfwuZpaBVEnpP5CSE62iGJjoliTuA8poQ==", + "requires": { + "codemirror": "^5.41.0", + "diff-match-patch": "^1.0.0" + } + }, "vue-hot-reload-api": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index a315b092..1677ccf6 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -18,6 +18,7 @@ "dependencies": { "js-cookie": "^2.2.0", "vue": "^2.6.10", + "vue-codemirror": "^4.0.6", "vue-hot-reload-api": "^2.3.3", "vue-router": "^3.0.6" }, diff --git a/dashboard/src/api.ts b/dashboard/src/api.ts index 6ef7481c..90bba8d5 100644 --- a/dashboard/src/api.ts +++ b/dashboard/src/api.ts @@ -1,3 +1,4 @@ +import { RootStore } from "./store"; const apiUrl = process.env.API_URL; type QueryParamObject = { [key: string]: string | null }; @@ -13,27 +14,14 @@ function buildQueryString(params: QueryParamObject) { ); } -let apiKey = null; -export function setApiKey(newKey) { - apiKey = newKey; -} -export function hasApiKey() { - return apiKey != null; -} -export function resetApiKey() { - apiKey = null; -} - export function request(resource, fetchOpts: RequestInit = {}) { - return fetch(`${apiUrl}/${resource}`, fetchOpts) - .then(res => res.json()) - .catch(err => { - console.log(err); - }); + return fetch(`${apiUrl}/${resource}`, fetchOpts).then(res => res.json()); } export function get(resource: string, params: QueryParamObject = {}) { - const headers: Record = apiKey ? { "X-Api-Key": (apiKey as unknown) as string } : {}; + const headers: Record = RootStore.state.auth.apiKey + ? { "X-Api-Key": RootStore.state.auth.apiKey } + : {}; return request(resource + buildQueryString(params), { method: "GET", headers, @@ -41,7 +29,9 @@ export function get(resource: string, params: QueryParamObject = {}) { } export function post(resource: string, params: QueryParamObject = {}) { - const headers: Record = apiKey ? { "X-Api-Key": (apiKey as unknown) as string } : {}; + const headers: Record = RootStore.state.auth.apiKey + ? { "X-Api-Key": RootStore.state.auth.apiKey } + : {}; return request(resource + buildQueryString(params), { method: "POST", body: JSON.stringify(params), diff --git a/dashboard/src/components/Dashboard.vue b/dashboard/src/components/Dashboard.vue index d68c9b49..3587d0fe 100644 --- a/dashboard/src/components/Dashboard.vue +++ b/dashboard/src/components/Dashboard.vue @@ -6,10 +6,10 @@

Guilds

- +
{{ guild.id }}{{ guild.guild_id }} {{ guild.name }} - Config + Config
@@ -17,19 +17,19 @@ diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts index ba8e8837..fd08c979 100644 --- a/dashboard/src/main.ts +++ b/dashboard/src/main.ts @@ -2,8 +2,6 @@ import Vue from "vue"; import { RootStore } from "./store"; import { router } from "./routes"; -get("/foo", { bar: "baz" }); - // Set up a read-only global variable to access specific env vars Vue.mixin({ data() { diff --git a/dashboard/src/routes.ts b/dashboard/src/routes.ts index 63f13a7a..e203ed57 100644 --- a/dashboard/src/routes.ts +++ b/dashboard/src/routes.ts @@ -3,8 +3,8 @@ import VueRouter, { RouteConfig } from "vue-router"; import Index from "./components/Index.vue"; import Login from "./components/Login.vue"; import LoginCallback from "./components/LoginCallback.vue"; +import GuildConfigEditor from "./components/GuildConfigEditor.vue"; import Dashboard from "./components/Dashboard.vue"; -import store from "./store"; import { authGuard, loginCallbackGuard } from "./auth"; Vue.use(VueRouter); @@ -15,7 +15,10 @@ const publicRoutes: RouteConfig[] = [ { path: "/login-callback", beforeEnter: loginCallbackGuard }, ]; -const authenticatedRoutes: RouteConfig[] = [{ path: "/dashboard", component: Dashboard }]; +const authenticatedRoutes: RouteConfig[] = [ + { path: "/dashboard", component: Dashboard }, + { path: "/dashboard/guilds/:guildId/config", component: GuildConfigEditor }, +]; authenticatedRoutes.forEach(route => { route.beforeEnter = authGuard; diff --git a/dashboard/src/store/auth.ts b/dashboard/src/store/auth.ts index 616367f2..a2757f18 100644 --- a/dashboard/src/store/auth.ts +++ b/dashboard/src/store/auth.ts @@ -16,11 +16,11 @@ export const AuthStore: Module = { const storedKey = localStorage.getItem("apiKey"); if (storedKey) { - console.log("key?", storedKey); const result = await post("auth/validate-key", { key: storedKey }); - if (result.isValid) { + if (result.valid) { await dispatch("setApiKey", storedKey); } else { + console.log("Unable to validate key, removing from localStorage"); localStorage.removeItem("apiKey"); } } diff --git a/dashboard/src/store/guilds.ts b/dashboard/src/store/guilds.ts index 709e320e..fb2367b1 100644 --- a/dashboard/src/store/guilds.ts +++ b/dashboard/src/store/guilds.ts @@ -1,4 +1,4 @@ -import { get } from "../api"; +import { get, post } from "../api"; import { Module } from "vuex"; import { GuildState, LoadStatus, RootState } from "./types"; @@ -19,6 +19,15 @@ export const GuildStore: Module = { const availableGuilds = await get("guilds/available"); commit("setAvailableGuilds", availableGuilds); }, + + async loadConfig({ commit }, guildId) { + const result = await get(`guilds/${guildId}/config`); + commit("setConfig", { guildId, config: result.config }); + }, + + async saveConfig({ commit }, { guildId, config }) { + await post(`guilds/${guildId}/config`, { config }); + }, }, mutations: { @@ -30,5 +39,9 @@ export const GuildStore: Module = { state.available = guilds; state.availableGuildsLoadStatus = LoadStatus.Done; }, + + setConfig(state: GuildState, { guildId, config }) { + state.configs[guildId] = config; + }, }, }; diff --git a/src/api/auth.ts b/src/api/auth.ts index 720c6b16..e960998d 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -59,13 +59,12 @@ export function initAuth(app: express.Express) { passport.use( "api-token", new CustomStrategy(async (req, cb) => { - console.log("in api-token strategy"); const apiKey = req.header("X-Api-Key"); if (!apiKey) return cb(); const userId = await dashboardLogins.getUserIdByApiKey(apiKey); if (userId) { - cb(null, { userId }); + return cb(null, { userId }); } cb(); @@ -111,9 +110,15 @@ export function initAuth(app: express.Express) { const userId = await dashboardLogins.getUserIdByApiKey(key); if (!userId) { - return res.status(403).json({ error: "Invalid key" }); + return res.json({ valid: false }); } - res.json({ status: "ok" }); + res.json({ valid: true }); + }); +} + +export function requireAPIToken(router: express.Router) { + router.use(passport.authenticate("api-token", { failWithError: true }), (err, req, res, next) => { + return res.json({ error: err.message }); }); } diff --git a/src/api/guilds.ts b/src/api/guilds.ts index e2a0db84..f51d6094 100644 --- a/src/api/guilds.ts +++ b/src/api/guilds.ts @@ -1,15 +1,43 @@ import express from "express"; import passport from "passport"; import { AllowedGuilds } from "../data/AllowedGuilds"; +import { requireAPIToken } from "./auth"; +import { DashboardUsers } from "../data/DashboardUsers"; +import { clientError, ok, unauthorized } from "./responses"; +import { Configs } from "../data/Configs"; +import { DashboardRoles } from "../data/DashboardRoles"; export function initGuildsAPI(app: express.Express) { const guildAPIRouter = express.Router(); - guildAPIRouter.use(passport.authenticate("api-token")); + requireAPIToken(guildAPIRouter); const allowedGuilds = new AllowedGuilds(); + const dashboardUsers = new DashboardUsers(); + const configs = new Configs(); guildAPIRouter.get("/guilds/available", async (req, res) => { const guilds = await allowedGuilds.getForDashboardUser(req.user.userId); - res.end(guilds); + res.json(guilds); }); + + guildAPIRouter.get("/guilds/:guildId/config", async (req, res) => { + const dbUser = await dashboardUsers.getByGuildAndUserId(req.params.guildId, req.user.userId); + if (!dbUser) return unauthorized(res); + + const config = await configs.getActiveByKey(`guild-${req.params.guildId}`); + res.json({ config: config ? config.config : "" }); + }); + + guildAPIRouter.post("/guilds/:guildId/config", async (req, res) => { + const dbUser = await dashboardUsers.getByGuildAndUserId(req.params.guildId, req.user.userId); + if (!dbUser || DashboardRoles[dbUser.role] < DashboardRoles.Editor) return unauthorized(res); + + const config = req.body.config; + if (config == null) return clientError(res, "No config supplied"); + + await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user.userId); + ok(res); + }); + + app.use(guildAPIRouter); } diff --git a/src/api/index.ts b/src/api/index.ts index 4e920a4f..eaac13c9 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -20,6 +20,11 @@ connect().then(() => { initAuth(app); initGuildsAPI(app); + app.use((err, req, res, next) => { + res.status(err.status || 500); + res.json({ error: err.message }); + }); + app.get("/", (req, res) => { res.end({ status: "cookies" }); }); diff --git a/src/api/responses.ts b/src/api/responses.ts new file mode 100644 index 00000000..976f3a15 --- /dev/null +++ b/src/api/responses.ts @@ -0,0 +1,13 @@ +import { Response } from "express"; + +export function unauthorized(res: Response) { + res.status(403).json({ error: "Unauthorized" }); +} + +export function clientError(res: Response, message: string) { + res.status(400).json({ error: message }); +} + +export function ok(res: Response) { + res.json({ result: "ok" }); +} diff --git a/src/data/DashboardUsers.ts b/src/data/DashboardUsers.ts index fa1dbd16..e0ce8489 100644 --- a/src/data/DashboardUsers.ts +++ b/src/data/DashboardUsers.ts @@ -9,4 +9,13 @@ export class DashboardUsers extends BaseRepository { super(); this.dashboardUsers = getRepository(DashboardUser); } + + getByGuildAndUserId(guildId, userId) { + return this.dashboardUsers.findOne({ + where: { + guild_id: guildId, + user_id: userId, + }, + }); + } }