mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-05-10 20:35:02 +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
|
@ -1,14 +1,14 @@
|
|||
import express from "express";
|
||||
import express, { Request, Response } from "express";
|
||||
import passport from "passport";
|
||||
import OAuth2Strategy from "passport-oauth2";
|
||||
import CustomStrategy from "passport-custom";
|
||||
import { DashboardLogins, DashboardLoginUserData } from "../../data/DashboardLogins";
|
||||
import { DashboardLogins, DashboardLoginUserData } from "../data/DashboardLogins";
|
||||
import pick from "lodash.pick";
|
||||
import https from "https";
|
||||
|
||||
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) => {
|
||||
const request = https.get(
|
||||
`${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());
|
||||
|
||||
if (!process.env.CLIENT_ID) {
|
||||
|
@ -84,10 +84,11 @@ export default function initAuth(app: express.Express) {
|
|||
scope: ["identify"],
|
||||
},
|
||||
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 apiKey = await dashboardLogins.addLogin(user.id, userData);
|
||||
// TODO: Revoke access token, we don't need it anymore
|
||||
console.log("done, calling cb with", apiKey);
|
||||
cb(null, { apiKey });
|
||||
},
|
||||
),
|
||||
|
@ -98,7 +99,21 @@ export default 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}`);
|
||||
},
|
||||
);
|
||||
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();
|
||||
|
||||
import express from "express";
|
||||
import initAuth from "./auth";
|
||||
import { connect } from "../../data/db";
|
||||
import cors from "cors";
|
||||
import { initAuth } from "./auth";
|
||||
import { initGuildsAPI } from "./guilds";
|
||||
import { connect } from "../data/db";
|
||||
|
||||
console.log("Connecting to database...");
|
||||
connect().then(() => {
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.DASHBOARD_URL,
|
||||
}),
|
||||
);
|
||||
app.use(express.json());
|
||||
|
||||
initAuth(app);
|
||||
initGuildsAPI(app);
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.end("Hi");
|
||||
res.end({ status: "cookies" });
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
|
@ -1,14 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Zeppelin Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<button id="login-button">Login</button>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -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,
|
||||
Unmute,
|
||||
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
|
||||
.createQueryBuilder()
|
||||
.where("id = :id", { id: loginId })
|
||||
.where("expires_at > NOW()")
|
||||
.andWhere("expires_at > NOW()")
|
||||
.getOne();
|
||||
|
||||
if (!login) {
|
||||
|
@ -40,7 +40,7 @@ export class DashboardLogins extends BaseRepository {
|
|||
}
|
||||
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(token);
|
||||
hash.update(loginId + token); // Remember to use loginId as the salt
|
||||
const hashedToken = hash.digest("hex");
|
||||
if (hashedToken !== login.token) {
|
||||
return null;
|
||||
|
@ -65,7 +65,7 @@ export class DashboardLogins extends BaseRepository {
|
|||
// Generate token
|
||||
const token = uuidv4();
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(token);
|
||||
hash.update(loginId + token); // Use loginId as a salt
|
||||
const hashedToken = hash.digest("hex");
|
||||
|
||||
// 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
|
||||
.createQueryBuilder()
|
||||
.where("id IN (:ids)", { ids })
|
||||
.where("deleted_at = :deletedAt", { deletedAt })
|
||||
.andWhere("deleted_at = :deletedAt", { deletedAt })
|
||||
.getMany();
|
||||
|
||||
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 DiscordHTTPError from "eris/lib/errors/DiscordHTTPError"; // tslint:disable-line
|
||||
|
||||
import { Configs } from "./data/Configs";
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
// Error handling
|
||||
|
@ -71,8 +73,8 @@ import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
|||
import { customArgumentTypes } from "./customArgumentTypes";
|
||||
import { errorMessage, successMessage } from "./utils";
|
||||
import { startUptimeCounter } from "./uptime";
|
||||
import { AllowedGuilds } from "./data/AllowedGuilds";
|
||||
|
||||
// Run latest database migrations
|
||||
logger.info("Connecting to database");
|
||||
connect().then(async conn => {
|
||||
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, {
|
||||
plugins: availablePlugins,
|
||||
globalPlugins: availableGlobalPlugins,
|
||||
|
||||
options: {
|
||||
canLoadGuild(guildId): Promise<boolean> {
|
||||
return allowedGuilds.isAllowed(guildId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Plugins are enabled if they...
|
||||
* - are base plugins, i.e. always enabled, or
|
||||
* - are dependencies of other enabled plugins, or
|
||||
* - are explicitly enabled in the guild config
|
||||
*/
|
||||
getEnabledPlugins(guildId, guildConfig): string[] {
|
||||
async getEnabledPlugins(guildId, guildConfig): Promise<string[]> {
|
||||
const configuredPlugins = guildConfig.plugins || {};
|
||||
const pluginNames: string[] = Array.from(this.plugins.keys());
|
||||
const plugins: Array<typeof Plugin> = Array.from(this.plugins.values());
|
||||
|
@ -125,22 +134,15 @@ connect().then(async conn => {
|
|||
return Array.from(finalEnabledPlugins.values());
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the requested config file from the config dir
|
||||
* TODO: Move to the database
|
||||
*/
|
||||
async getConfig(id) {
|
||||
const configFile = id ? `${id}.yml` : "global.yml";
|
||||
const configPath = path.join("config", configFile);
|
||||
|
||||
try {
|
||||
await fsp.access(configPath);
|
||||
} catch (e) {
|
||||
return {};
|
||||
const key = id === "global" ? "global" : `guild-${id}`;
|
||||
const row = await guildConfigs.getActiveByKey(key);
|
||||
if (row) {
|
||||
return yaml.safeLoad(row.config);
|
||||
}
|
||||
|
||||
const yamlString = await fsp.readFile(configPath, { encoding: "utf8" });
|
||||
return yaml.safeLoad(yamlString);
|
||||
logger.warn(`No config with key "${key}"`);
|
||||
return {};
|
||||
},
|
||||
|
||||
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
Add a link
Reference in a new issue