mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
Dashboard work. Move configs to DB. Some script reorganization. Add nodemon configs.
This commit is contained in:
parent
168d82a966
commit
2adc5af8d7
39 changed files with 8441 additions and 2915 deletions
8
dashboard/.babelrc
Normal file
8
dashboard/.babelrc
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
["transform-runtime", {
|
||||||
|
"regenerator": true
|
||||||
|
}],
|
||||||
|
"transform-object-rest-spread"
|
||||||
|
]
|
||||||
|
}
|
3
dashboard/.gitignore
vendored
Normal file
3
dashboard/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/.cache
|
||||||
|
/dist
|
||||||
|
/node_modules
|
7668
dashboard/package-lock.json
generated
Normal file
7668
dashboard/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
dashboard/package.json
Normal file
27
dashboard/package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "zeppelin-dashboard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "rimraf dist && parcel build src/index.html --out-dir dist",
|
||||||
|
"watch": "parcel src/index.html"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/component-compiler-utils": "^3.0.0",
|
||||||
|
"babel-core": "^6.26.3",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
|
"parcel-bundler": "^1.12.3",
|
||||||
|
"vue-template-compiler": "^2.6.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"js-cookie": "^2.2.0",
|
||||||
|
"vue": "^2.6.10",
|
||||||
|
"vue-hot-reload-api": "^2.3.3",
|
||||||
|
"vue-router": "^3.0.6"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"last 2 Chrome versions"
|
||||||
|
]
|
||||||
|
}
|
53
dashboard/src/api.ts
Normal file
53
dashboard/src/api.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
const apiUrl = process.env.API_URL;
|
||||||
|
|
||||||
|
type QueryParamObject = { [key: string]: string | null };
|
||||||
|
|
||||||
|
function buildQueryString(params: QueryParamObject) {
|
||||||
|
if (Object.keys(params).length === 0) return "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
"?" +
|
||||||
|
Array.from(Object.entries(params))
|
||||||
|
.map(pair => `${encodeURIComponent(pair[0])}=${encodeURIComponent(pair[1] || "")}`)
|
||||||
|
.join("&")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get(resource: string, params: QueryParamObject = {}) {
|
||||||
|
const headers: Record<string, string> = apiKey ? { "X-Api-Key": (apiKey as unknown) as string } : {};
|
||||||
|
return request(resource + buildQueryString(params), {
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function post(resource: string, params: QueryParamObject = {}) {
|
||||||
|
const headers: Record<string, string> = apiKey ? { "X-Api-Key": (apiKey as unknown) as string } : {};
|
||||||
|
return request(resource + buildQueryString(params), {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
15
dashboard/src/auth.ts
Normal file
15
dashboard/src/auth.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { NavigationGuard } from "vue-router";
|
||||||
|
import { RootStore } from "./store";
|
||||||
|
|
||||||
|
export const authGuard: NavigationGuard = async (to, from, next) => {
|
||||||
|
if (RootStore.state.auth.apiKey) return next(); // We have an API key -> authenticated
|
||||||
|
if (RootStore.state.auth.loadedInitialAuth) return next("/login"); // No API key and initial auth data was already loaded -> not authenticated
|
||||||
|
await RootStore.dispatch("auth/loadInitialAuth"); // Initial auth data wasn't loaded yet (per above check) -> load it now
|
||||||
|
if (RootStore.state.auth.apiKey) return next();
|
||||||
|
next("/login"); // Still no API key -> not authenticated
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
|
||||||
|
await RootStore.dispatch("auth/setApiKey", to.query.apiKey);
|
||||||
|
next("/dashboard");
|
||||||
|
};
|
3
dashboard/src/components/App.vue
Normal file
3
dashboard/src/components/App.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<router-view></router-view>
|
||||||
|
</template>
|
36
dashboard/src/components/Dashboard.vue
Normal file
36
dashboard/src/components/Dashboard.vue
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="loading">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h1>Guilds</h1>
|
||||||
|
<table v-for="guild in guilds">
|
||||||
|
<tr>
|
||||||
|
<td>{{ guild.id }}</td>
|
||||||
|
<td>{{ guild.name }}</td>
|
||||||
|
<td>
|
||||||
|
<a v-bind:href="'/dashboard/guilds/' + guild.id + '/config'">Config</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, mapState} from "vuex";
|
||||||
|
import {LoadStatus} from "../store/types";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch("guilds/loadAvailableGuilds");
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
loading() {
|
||||||
|
return this.$state.guilds.availableGuildsLoadStatus !== LoadStatus.Done;
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
guilds: 'guilds/available',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
11
dashboard/src/components/Index.vue
Normal file
11
dashboard/src/components/Index.vue
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<p>Redirecting...</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
created() {
|
||||||
|
this.$router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
14
dashboard/src/components/Login.vue
Normal file
14
dashboard/src/components/Login.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<a v-bind:href="env.API_URL + '/auth/login'">Login</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
blah() {
|
||||||
|
return this.$state.apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -8,7 +8,8 @@
|
||||||
<title>Zeppelin Dashboard</title>
|
<title>Zeppelin Dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<button id="login-button">Login</button>
|
<div id="app"></div>
|
||||||
<script src="./index.js"></script>
|
|
||||||
|
<script src="./main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
31
dashboard/src/main.ts
Normal file
31
dashboard/src/main.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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() {
|
||||||
|
return {
|
||||||
|
get env() {
|
||||||
|
return Object.freeze({
|
||||||
|
API_URL: process.env.API_URL,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
import App from "./components/App.vue";
|
||||||
|
import Login from "./components/Login.vue";
|
||||||
|
import { get } from "./api";
|
||||||
|
|
||||||
|
const app = new Vue({
|
||||||
|
router,
|
||||||
|
store: RootStore,
|
||||||
|
el: "#app",
|
||||||
|
render(h) {
|
||||||
|
return h(App);
|
||||||
|
},
|
||||||
|
});
|
27
dashboard/src/routes.ts
Normal file
27
dashboard/src/routes.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import Vue from "vue";
|
||||||
|
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 Dashboard from "./components/Dashboard.vue";
|
||||||
|
import store from "./store";
|
||||||
|
import { authGuard, loginCallbackGuard } from "./auth";
|
||||||
|
|
||||||
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
|
const publicRoutes: RouteConfig[] = [
|
||||||
|
{ path: "/", component: Index },
|
||||||
|
{ path: "/login", component: Login },
|
||||||
|
{ path: "/login-callback", beforeEnter: loginCallbackGuard },
|
||||||
|
];
|
||||||
|
|
||||||
|
const authenticatedRoutes: RouteConfig[] = [{ path: "/dashboard", component: Dashboard }];
|
||||||
|
|
||||||
|
authenticatedRoutes.forEach(route => {
|
||||||
|
route.beforeEnter = authGuard;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const router = new VueRouter({
|
||||||
|
mode: "history",
|
||||||
|
routes: [...publicRoutes, ...authenticatedRoutes],
|
||||||
|
});
|
46
dashboard/src/store/auth.ts
Normal file
46
dashboard/src/store/auth.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { get, hasApiKey, post, setApiKey } from "../api";
|
||||||
|
import { ActionTree, Module } from "vuex";
|
||||||
|
import { AuthState, RootState } from "./types";
|
||||||
|
|
||||||
|
export const AuthStore: Module<AuthState, RootState> = {
|
||||||
|
namespaced: true,
|
||||||
|
|
||||||
|
state: {
|
||||||
|
apiKey: null,
|
||||||
|
loadedInitialAuth: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async loadInitialAuth({ dispatch, commit, state }) {
|
||||||
|
if (state.loadedInitialAuth) return;
|
||||||
|
|
||||||
|
const storedKey = localStorage.getItem("apiKey");
|
||||||
|
if (storedKey) {
|
||||||
|
console.log("key?", storedKey);
|
||||||
|
const result = await post("auth/validate-key", { key: storedKey });
|
||||||
|
if (result.isValid) {
|
||||||
|
await dispatch("setApiKey", storedKey);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("apiKey");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commit("markInitialAuthLoaded");
|
||||||
|
},
|
||||||
|
|
||||||
|
setApiKey({ commit, state }, newKey: string) {
|
||||||
|
localStorage.setItem("apiKey", newKey);
|
||||||
|
commit("setApiKey", newKey);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mutations: {
|
||||||
|
setApiKey(state: AuthState, key) {
|
||||||
|
state.apiKey = key;
|
||||||
|
},
|
||||||
|
|
||||||
|
markInitialAuthLoaded(state: AuthState) {
|
||||||
|
state.loadedInitialAuth = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
34
dashboard/src/store/guilds.ts
Normal file
34
dashboard/src/store/guilds.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { get } from "../api";
|
||||||
|
import { Module } from "vuex";
|
||||||
|
import { GuildState, LoadStatus, RootState } from "./types";
|
||||||
|
|
||||||
|
export const GuildStore: Module<GuildState, RootState> = {
|
||||||
|
namespaced: true,
|
||||||
|
|
||||||
|
state: {
|
||||||
|
availableGuildsLoadStatus: LoadStatus.None,
|
||||||
|
available: [],
|
||||||
|
configs: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async loadAvailableGuilds({ dispatch, commit, state }) {
|
||||||
|
if (state.availableGuildsLoadStatus !== LoadStatus.None) return;
|
||||||
|
commit("setAvailableGuildsLoadStatus", LoadStatus.Loading);
|
||||||
|
|
||||||
|
const availableGuilds = await get("guilds/available");
|
||||||
|
commit("setAvailableGuilds", availableGuilds);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mutations: {
|
||||||
|
setAvailableGuildsLoadStatus(state: GuildState, status: LoadStatus) {
|
||||||
|
state.availableGuildsLoadStatus = status;
|
||||||
|
},
|
||||||
|
|
||||||
|
setAvailableGuilds(state: GuildState, guilds) {
|
||||||
|
state.available = guilds;
|
||||||
|
state.availableGuildsLoadStatus = LoadStatus.Done;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
28
dashboard/src/store/index.ts
Normal file
28
dashboard/src/store/index.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import Vue from "vue";
|
||||||
|
import Vuex, { Store } from "vuex";
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
import { RootState } from "./types";
|
||||||
|
import { AuthStore } from "./auth";
|
||||||
|
import { GuildStore } from "./guilds";
|
||||||
|
|
||||||
|
export const RootStore = new Vuex.Store<RootState>({
|
||||||
|
modules: {
|
||||||
|
auth: AuthStore,
|
||||||
|
guilds: GuildStore,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up typings so Vue/our components know about the state's types
|
||||||
|
declare module "vue/types/options" {
|
||||||
|
interface ComponentOptions<V extends Vue> {
|
||||||
|
store?: Store<RootState>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "vue/types/vue" {
|
||||||
|
interface Vue {
|
||||||
|
$store: Store<RootState>;
|
||||||
|
}
|
||||||
|
}
|
27
dashboard/src/store/types.ts
Normal file
27
dashboard/src/store/types.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
export enum LoadStatus {
|
||||||
|
None = 1,
|
||||||
|
Loading,
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthState {
|
||||||
|
apiKey: string | null;
|
||||||
|
loadedInitialAuth: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuildState {
|
||||||
|
availableGuildsLoadStatus: LoadStatus;
|
||||||
|
available: Array<{
|
||||||
|
guild_id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string | null;
|
||||||
|
}>;
|
||||||
|
configs: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RootState = {
|
||||||
|
auth: AuthState;
|
||||||
|
guilds: GuildState;
|
||||||
|
};
|
4
dashboard/ts-vue-shim.d.ts
vendored
Normal file
4
dashboard/ts-vue-shim.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
declare module "*.vue" {
|
||||||
|
import Vue from "vue";
|
||||||
|
export default Vue;
|
||||||
|
}
|
28
dashboard/tsconfig.json
Normal file
28
dashboard/tsconfig.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"module": "es2015",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"target": "esnext",
|
||||||
|
"strict": true,
|
||||||
|
"lib": [
|
||||||
|
"es2017",
|
||||||
|
"esnext",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"baseUrl": "./",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.vue"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"ts-vue-shim.d.ts"
|
||||||
|
]
|
||||||
|
}
|
5
nodemon-api.json
Normal file
5
nodemon-api.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"watch": "src",
|
||||||
|
"ext": "ts",
|
||||||
|
"exec": "ts-node ./src/api/index.ts"
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"watch": "src/dashboard/api",
|
|
||||||
"ext": "ts",
|
|
||||||
"exec": "ts-node ./src/dashboard/api/index.ts"
|
|
||||||
}
|
|
2888
package-lock.json
generated
2888
package-lock.json
generated
File diff suppressed because it is too large
Load diff
26
package.json
26
package.json
|
@ -4,27 +4,26 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest src",
|
|
||||||
"start-bot-dev": "ts-node src/index.ts",
|
"start-bot-dev": "ts-node src/index.ts",
|
||||||
"start-bot-watch": "nodemon --config nodemon-bot.json",
|
|
||||||
"start-bot-prod": "cross-env NODE_ENV=production node dist/index.js",
|
"start-bot-prod": "cross-env NODE_ENV=production node dist/index.js",
|
||||||
"format": "prettier --write \"./**/*.ts\"",
|
"watch-bot": "nodemon --config nodemon-bot.json",
|
||||||
"typeorm": "ts-node ./node_modules/typeorm/cli.js",
|
|
||||||
"build": "rimraf dist && tsc",
|
"build": "rimraf dist && tsc",
|
||||||
"build-dashboard-frontend": "rimraf dist-frontend && parcel build src/dashboard/frontend/index.html --out-dir dist-frontend",
|
"start-api-dev": "ts-node src/api/index.ts",
|
||||||
"start-dashboard-frontend-dev": "parcel src/dashboard/frontend/index.html",
|
"start-api-prod": "cross-env NODE_ENV=production node dist/api/index.js",
|
||||||
"start-dashboard-api-dev": "ts-node src/dashboard/server/index.ts",
|
"watch-api": "nodemon --config nodemon-api.json",
|
||||||
"start-dashboard-api-watch": "nodemon --config nodemon-dashboard-api.json",
|
"format": "prettier --write \"./src/**/*.ts\"",
|
||||||
"start-dashboard-api": "cross-env NODE_ENV=production node dist/dashboard/server/index.js",
|
"typeorm": "ts-node ./node_modules/typeorm/cli.js",
|
||||||
"migrate": "npm run typeorm -- migration:run",
|
"migrate": "npm run typeorm -- migration:run",
|
||||||
"migrate-rollback": "npm run typeorm -- migration:revert"
|
"migrate-rollback": "npm run typeorm -- migration:revert",
|
||||||
|
"test": "jest src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.7.0",
|
"ajv": "^6.7.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
"emoji-regex": "^7.0.1",
|
"emoji-regex": "^7.0.1",
|
||||||
"eris": "github:abalabahaha/eris#dev",
|
"eris": "^0.10.0",
|
||||||
"escape-string-regexp": "^1.0.5",
|
"escape-string-regexp": "^1.0.5",
|
||||||
"express": "^4.17.0",
|
"express": "^4.17.0",
|
||||||
"humanize-duration": "^3.15.0",
|
"humanize-duration": "^3.15.0",
|
||||||
|
@ -48,12 +47,14 @@
|
||||||
"ts-node": "^3.3.0",
|
"ts-node": "^3.3.0",
|
||||||
"typeorm": "^0.2.14",
|
"typeorm": "^0.2.14",
|
||||||
"typescript": "^3.3.3333",
|
"typescript": "^3.3.3333",
|
||||||
"uuid": "^3.3.2"
|
"uuid": "^3.3.2",
|
||||||
|
"vuex": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.3.4",
|
"@babel/core": "^7.3.4",
|
||||||
"@babel/preset-env": "^7.3.4",
|
"@babel/preset-env": "^7.3.4",
|
||||||
"@babel/preset-typescript": "^7.3.3",
|
"@babel/preset-typescript": "^7.3.3",
|
||||||
|
"@types/cors": "^2.8.5",
|
||||||
"@types/express": "^4.16.1",
|
"@types/express": "^4.16.1",
|
||||||
"@types/jest": "^24.0.11",
|
"@types/jest": "^24.0.11",
|
||||||
"@types/lodash.at": "^4.6.3",
|
"@types/lodash.at": "^4.6.3",
|
||||||
|
@ -67,7 +68,6 @@
|
||||||
"jest": "^24.7.1",
|
"jest": "^24.7.1",
|
||||||
"lint-staged": "^8.1.5",
|
"lint-staged": "^8.1.5",
|
||||||
"nodemon": "^1.17.5",
|
"nodemon": "^1.17.5",
|
||||||
"parcel-bundler": "^1.12.3",
|
|
||||||
"prettier": "^1.16.4",
|
"prettier": "^1.16.4",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"tslint": "^5.13.1",
|
"tslint": "^5.13.1",
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import express from "express";
|
import express, { Request, Response } from "express";
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
import OAuth2Strategy from "passport-oauth2";
|
import OAuth2Strategy from "passport-oauth2";
|
||||||
import CustomStrategy from "passport-custom";
|
import CustomStrategy from "passport-custom";
|
||||||
import { DashboardLogins, DashboardLoginUserData } from "../../data/DashboardLogins";
|
import { DashboardLogins, DashboardLoginUserData } from "../data/DashboardLogins";
|
||||||
import pick from "lodash.pick";
|
import pick from "lodash.pick";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
|
|
||||||
const DISCORD_API_URL = "https://discordapp.com/api";
|
const DISCORD_API_URL = "https://discordapp.com/api";
|
||||||
|
|
||||||
function simpleAPIRequest(bearerToken, path): Promise<any> {
|
function simpleDiscordAPIRequest(bearerToken, path): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = https.get(
|
const request = https.get(
|
||||||
`${DISCORD_API_URL}/${path}`,
|
`${DISCORD_API_URL}/${path}`,
|
||||||
|
@ -31,7 +31,7 @@ function simpleAPIRequest(bearerToken, path): Promise<any> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function initAuth(app: express.Express) {
|
export function initAuth(app: express.Express) {
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
|
|
||||||
if (!process.env.CLIENT_ID) {
|
if (!process.env.CLIENT_ID) {
|
||||||
|
@ -84,10 +84,11 @@ export default function initAuth(app: express.Express) {
|
||||||
scope: ["identify"],
|
scope: ["identify"],
|
||||||
},
|
},
|
||||||
async (accessToken, refreshToken, profile, cb) => {
|
async (accessToken, refreshToken, profile, cb) => {
|
||||||
const user = await simpleAPIRequest(accessToken, "users/@me");
|
const user = await simpleDiscordAPIRequest(accessToken, "users/@me");
|
||||||
const userData = pick(user, ["username", "discriminator", "avatar"]) as DashboardLoginUserData;
|
const userData = pick(user, ["username", "discriminator", "avatar"]) as DashboardLoginUserData;
|
||||||
const apiKey = await dashboardLogins.addLogin(user.id, userData);
|
const apiKey = await dashboardLogins.addLogin(user.id, userData);
|
||||||
// TODO: Revoke access token, we don't need it anymore
|
// TODO: Revoke access token, we don't need it anymore
|
||||||
|
console.log("done, calling cb with", apiKey);
|
||||||
cb(null, { apiKey });
|
cb(null, { apiKey });
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -98,7 +99,21 @@ export default 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");
|
||||||
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
|
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
app.post("/auth/validate-key", async (req: Request, res: Response) => {
|
||||||
|
const key = req.params.key || req.query.key;
|
||||||
|
if (!key) {
|
||||||
|
return res.status(400).json({ error: "No key supplied" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = await dashboardLogins.getUserIdByApiKey(key);
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(403).json({ error: "Invalid key" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ status: "ok" });
|
||||||
|
});
|
||||||
}
|
}
|
15
src/api/guilds.ts
Normal file
15
src/api/guilds.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import express from "express";
|
||||||
|
import passport from "passport";
|
||||||
|
import { AllowedGuilds } from "../data/AllowedGuilds";
|
||||||
|
|
||||||
|
export function initGuildsAPI(app: express.Express) {
|
||||||
|
const guildAPIRouter = express.Router();
|
||||||
|
guildAPIRouter.use(passport.authenticate("api-token"));
|
||||||
|
|
||||||
|
const allowedGuilds = new AllowedGuilds();
|
||||||
|
|
||||||
|
guildAPIRouter.get("/guilds/available", async (req, res) => {
|
||||||
|
const guilds = await allowedGuilds.getForDashboardUser(req.user.userId);
|
||||||
|
res.end(guilds);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,17 +1,27 @@
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import initAuth from "./auth";
|
import cors from "cors";
|
||||||
import { connect } from "../../data/db";
|
import { initAuth } from "./auth";
|
||||||
|
import { initGuildsAPI } from "./guilds";
|
||||||
|
import { connect } from "../data/db";
|
||||||
|
|
||||||
console.log("Connecting to database...");
|
console.log("Connecting to database...");
|
||||||
connect().then(() => {
|
connect().then(() => {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: process.env.DASHBOARD_URL,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
initAuth(app);
|
initAuth(app);
|
||||||
|
initGuildsAPI(app);
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
res.end("Hi");
|
res.end({ status: "cookies" });
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
|
@ -1,4 +0,0 @@
|
||||||
const API_URL = process.env.API_URL;
|
|
||||||
document.getElementById('login-button').addEventListener('click', () => {
|
|
||||||
window.location.href = `${API_URL}/auth/login`;
|
|
||||||
});
|
|
41
src/data/AllowedGuilds.ts
Normal file
41
src/data/AllowedGuilds.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { AllowedGuild } from "./entities/AllowedGuild";
|
||||||
|
import {
|
||||||
|
getConnection,
|
||||||
|
getRepository,
|
||||||
|
Repository,
|
||||||
|
Transaction,
|
||||||
|
TransactionManager,
|
||||||
|
TransactionRepository,
|
||||||
|
} from "typeorm";
|
||||||
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
|
||||||
|
export class AllowedGuilds extends BaseRepository {
|
||||||
|
private allowedGuilds: Repository<AllowedGuild>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.allowedGuilds = getRepository(AllowedGuild);
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAllowed(guildId) {
|
||||||
|
const count = await this.allowedGuilds.count({
|
||||||
|
where: {
|
||||||
|
guild_id: guildId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return count !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getForDashboardUser(userId) {
|
||||||
|
return this.allowedGuilds
|
||||||
|
.createQueryBuilder("allowed_guilds")
|
||||||
|
.innerJoin(
|
||||||
|
"dashboard_users",
|
||||||
|
"dashboard_users",
|
||||||
|
"dashboard_users.guild_id = allowed_guilds.guild_id AND dashboard_users.user_id = :userId",
|
||||||
|
{ userId },
|
||||||
|
)
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,5 +7,5 @@ export enum CaseTypes {
|
||||||
Mute,
|
Mute,
|
||||||
Unmute,
|
Unmute,
|
||||||
Expunged,
|
Expunged,
|
||||||
Softban
|
Softban,
|
||||||
}
|
}
|
||||||
|
|
49
src/data/Configs.ts
Normal file
49
src/data/Configs.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { Config } from "./entities/Config";
|
||||||
|
import {
|
||||||
|
getConnection,
|
||||||
|
getRepository,
|
||||||
|
Repository,
|
||||||
|
Transaction,
|
||||||
|
TransactionManager,
|
||||||
|
TransactionRepository,
|
||||||
|
} from "typeorm";
|
||||||
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { connection } from "./db";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
|
||||||
|
export class Configs extends BaseRepository {
|
||||||
|
private configs: Repository<Config>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.configs = getRepository(Config);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveByKey(key) {
|
||||||
|
return this.configs.findOne({
|
||||||
|
where: {
|
||||||
|
key,
|
||||||
|
is_active: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasConfig(key) {
|
||||||
|
return (await this.getActiveByKey(key)) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveNewRevision(key, config, editedBy) {
|
||||||
|
return connection.transaction(async entityManager => {
|
||||||
|
const repo = entityManager.getRepository(Config);
|
||||||
|
// Mark all old revisions inactive
|
||||||
|
await repo.update({ key }, { is_active: false });
|
||||||
|
// Add new, active revision
|
||||||
|
await repo.insert({
|
||||||
|
key,
|
||||||
|
config,
|
||||||
|
is_active: true,
|
||||||
|
edited_by: editedBy,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ export class DashboardLogins extends BaseRepository {
|
||||||
const login = await this.dashboardLogins
|
const login = await this.dashboardLogins
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.where("id = :id", { id: loginId })
|
.where("id = :id", { id: loginId })
|
||||||
.where("expires_at > NOW()")
|
.andWhere("expires_at > NOW()")
|
||||||
.getOne();
|
.getOne();
|
||||||
|
|
||||||
if (!login) {
|
if (!login) {
|
||||||
|
@ -40,7 +40,7 @@ export class DashboardLogins extends BaseRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = crypto.createHash("sha256");
|
const hash = crypto.createHash("sha256");
|
||||||
hash.update(token);
|
hash.update(loginId + token); // Remember to use loginId as the salt
|
||||||
const hashedToken = hash.digest("hex");
|
const hashedToken = hash.digest("hex");
|
||||||
if (hashedToken !== login.token) {
|
if (hashedToken !== login.token) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -65,7 +65,7 @@ export class DashboardLogins extends BaseRepository {
|
||||||
// Generate token
|
// Generate token
|
||||||
const token = uuidv4();
|
const token = uuidv4();
|
||||||
const hash = crypto.createHash("sha256");
|
const hash = crypto.createHash("sha256");
|
||||||
hash.update(token);
|
hash.update(loginId + token); // Use loginId as a salt
|
||||||
const hashedToken = hash.digest("hex");
|
const hashedToken = hash.digest("hex");
|
||||||
|
|
||||||
// Save this to the DB
|
// Save this to the DB
|
||||||
|
|
6
src/data/DashboardRoles.ts
Normal file
6
src/data/DashboardRoles.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export enum DashboardRoles {
|
||||||
|
Viewer = 1,
|
||||||
|
Editor,
|
||||||
|
Manager,
|
||||||
|
ServerOwner,
|
||||||
|
}
|
|
@ -201,7 +201,7 @@ export class GuildSavedMessages extends BaseGuildRepository {
|
||||||
const deleted = await this.messages
|
const deleted = await this.messages
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.where("id IN (:ids)", { ids })
|
.where("id IN (:ids)", { ids })
|
||||||
.where("deleted_at = :deletedAt", { deletedAt })
|
.andWhere("deleted_at = :deletedAt", { deletedAt })
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
if (deleted.length) {
|
if (deleted.length) {
|
||||||
|
|
14
src/data/entities/AllowedGuild.ts
Normal file
14
src/data/entities/AllowedGuild.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm";
|
||||||
|
|
||||||
|
@Entity("allowed_guilds")
|
||||||
|
export class AllowedGuild {
|
||||||
|
@Column()
|
||||||
|
@PrimaryColumn()
|
||||||
|
guild_id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
icon: string;
|
||||||
|
}
|
23
src/data/entities/Config.ts
Normal file
23
src/data/entities/Config.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm";
|
||||||
|
|
||||||
|
@Entity("configs")
|
||||||
|
export class Config {
|
||||||
|
@Column()
|
||||||
|
@PrimaryColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
config: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
is_active: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
edited_by: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
edited_at: string;
|
||||||
|
}
|
32
src/index.ts
32
src/index.ts
|
@ -10,6 +10,8 @@ import { SimpleError } from "./SimpleError";
|
||||||
import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line
|
import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line
|
||||||
import DiscordHTTPError from "eris/lib/errors/DiscordHTTPError"; // tslint:disable-line
|
import DiscordHTTPError from "eris/lib/errors/DiscordHTTPError"; // tslint:disable-line
|
||||||
|
|
||||||
|
import { Configs } from "./data/Configs";
|
||||||
|
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
|
@ -71,8 +73,8 @@ import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
||||||
import { customArgumentTypes } from "./customArgumentTypes";
|
import { customArgumentTypes } from "./customArgumentTypes";
|
||||||
import { errorMessage, successMessage } from "./utils";
|
import { errorMessage, successMessage } from "./utils";
|
||||||
import { startUptimeCounter } from "./uptime";
|
import { startUptimeCounter } from "./uptime";
|
||||||
|
import { AllowedGuilds } from "./data/AllowedGuilds";
|
||||||
|
|
||||||
// Run latest database migrations
|
|
||||||
logger.info("Connecting to database");
|
logger.info("Connecting to database");
|
||||||
connect().then(async conn => {
|
connect().then(async conn => {
|
||||||
const client = new Client(`Bot ${process.env.TOKEN}`, {
|
const client = new Client(`Bot ${process.env.TOKEN}`, {
|
||||||
|
@ -87,18 +89,25 @@ connect().then(async conn => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const allowedGuilds = new AllowedGuilds();
|
||||||
|
const guildConfigs = new Configs();
|
||||||
|
|
||||||
const bot = new Knub(client, {
|
const bot = new Knub(client, {
|
||||||
plugins: availablePlugins,
|
plugins: availablePlugins,
|
||||||
globalPlugins: availableGlobalPlugins,
|
globalPlugins: availableGlobalPlugins,
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
|
canLoadGuild(guildId): Promise<boolean> {
|
||||||
|
return allowedGuilds.isAllowed(guildId);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugins are enabled if they...
|
* Plugins are enabled if they...
|
||||||
* - are base plugins, i.e. always enabled, or
|
* - are base plugins, i.e. always enabled, or
|
||||||
* - are dependencies of other enabled plugins, or
|
* - are dependencies of other enabled plugins, or
|
||||||
* - are explicitly enabled in the guild config
|
* - are explicitly enabled in the guild config
|
||||||
*/
|
*/
|
||||||
getEnabledPlugins(guildId, guildConfig): string[] {
|
async getEnabledPlugins(guildId, guildConfig): Promise<string[]> {
|
||||||
const configuredPlugins = guildConfig.plugins || {};
|
const configuredPlugins = guildConfig.plugins || {};
|
||||||
const pluginNames: string[] = Array.from(this.plugins.keys());
|
const pluginNames: string[] = Array.from(this.plugins.keys());
|
||||||
const plugins: Array<typeof Plugin> = Array.from(this.plugins.values());
|
const plugins: Array<typeof Plugin> = Array.from(this.plugins.values());
|
||||||
|
@ -125,22 +134,15 @@ connect().then(async conn => {
|
||||||
return Array.from(finalEnabledPlugins.values());
|
return Array.from(finalEnabledPlugins.values());
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the requested config file from the config dir
|
|
||||||
* TODO: Move to the database
|
|
||||||
*/
|
|
||||||
async getConfig(id) {
|
async getConfig(id) {
|
||||||
const configFile = id ? `${id}.yml` : "global.yml";
|
const key = id === "global" ? "global" : `guild-${id}`;
|
||||||
const configPath = path.join("config", configFile);
|
const row = await guildConfigs.getActiveByKey(key);
|
||||||
|
if (row) {
|
||||||
try {
|
return yaml.safeLoad(row.config);
|
||||||
await fsp.access(configPath);
|
|
||||||
} catch (e) {
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const yamlString = await fsp.readFile(configPath, { encoding: "utf8" });
|
logger.warn(`No config with key "${key}"`);
|
||||||
return yaml.safeLoad(yamlString);
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
logFn: (level, msg) => {
|
logFn: (level, msg) => {
|
||||||
|
|
39
src/migrateConfigsToDB.ts
Normal file
39
src/migrateConfigsToDB.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { connect } from "./data/db";
|
||||||
|
import { Configs } from "./data/Configs";
|
||||||
|
import path from "path";
|
||||||
|
import * as _fs from "fs";
|
||||||
|
const fs = _fs.promises;
|
||||||
|
|
||||||
|
const authorId = process.argv[2];
|
||||||
|
if (!authorId) {
|
||||||
|
console.error("No author id specified");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Connecting to database");
|
||||||
|
connect().then(async () => {
|
||||||
|
const configs = new Configs();
|
||||||
|
|
||||||
|
console.log("Loading config files");
|
||||||
|
const configDir = path.join(__dirname, "..", "config");
|
||||||
|
const configFiles = await fs.readdir(configDir);
|
||||||
|
|
||||||
|
console.log("Looping through config files");
|
||||||
|
for (const configFile of configFiles) {
|
||||||
|
const parts = configFile.split(".");
|
||||||
|
const ext = parts[parts.length - 1];
|
||||||
|
if (ext !== "yml") continue;
|
||||||
|
|
||||||
|
const id = parts.slice(0, -1).join(".");
|
||||||
|
const key = id === "global" ? "global" : `guild-${id}`;
|
||||||
|
if (await configs.hasConfig(key)) continue;
|
||||||
|
|
||||||
|
const content = await fs.readFile(path.join(configDir, configFile), { encoding: "utf8" });
|
||||||
|
|
||||||
|
console.log(`Migrating config for ${key}`);
|
||||||
|
await configs.saveNewRevision(key, content, authorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Done!");
|
||||||
|
process.exit(0);
|
||||||
|
});
|
51
src/migrations/1561111990357-CreateConfigsTable.ts
Normal file
51
src/migrations/1561111990357-CreateConfigsTable.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";
|
||||||
|
|
||||||
|
export class CreateConfigsTable1561111990357 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: "configs",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "int",
|
||||||
|
isPrimary: true,
|
||||||
|
isGenerated: true,
|
||||||
|
generationStrategy: "increment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key",
|
||||||
|
type: "varchar",
|
||||||
|
length: "48",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config",
|
||||||
|
type: "mediumtext",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is_active",
|
||||||
|
type: "tinyint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "edited_by",
|
||||||
|
type: "bigint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "edited_at",
|
||||||
|
type: "datetime",
|
||||||
|
default: "now()",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indices: [
|
||||||
|
{
|
||||||
|
columnNames: ["key", "is_active"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.dropTable("configs", true);
|
||||||
|
}
|
||||||
|
}
|
39
src/migrations/1561117545258-CreateAllowedGuildsTable.ts
Normal file
39
src/migrations/1561117545258-CreateAllowedGuildsTable.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { MigrationInterface, QueryRunner, Table } from "typeorm";
|
||||||
|
|
||||||
|
export class CreateAllowedGuildsTable1561117545258 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: "allowed_guilds",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "guild_id",
|
||||||
|
type: "bigint",
|
||||||
|
isPrimary: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
type: "varchar",
|
||||||
|
length: "255",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "icon",
|
||||||
|
type: "varchar",
|
||||||
|
length: "255",
|
||||||
|
collation: "ascii_general_ci",
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "owner_id",
|
||||||
|
type: "bigint",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indices: [{ columnNames: ["owner_id"] }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.dropTable("allowed_guilds", true);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue