3
0
Fork 0
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:
Dragory 2019-06-22 18:52:24 +03:00
parent 168d82a966
commit 2adc5af8d7
39 changed files with 8441 additions and 2915 deletions

View file

@ -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
View 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);
});
}

View file

@ -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;

View file

@ -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>

View file

@ -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
View 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();
}
}

View file

@ -7,5 +7,5 @@ export enum CaseTypes {
Mute,
Unmute,
Expunged,
Softban
Softban,
}

49
src/data/Configs.ts Normal file
View 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,
});
});
}
}

View file

@ -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

View file

@ -0,0 +1,6 @@
export enum DashboardRoles {
Viewer = 1,
Editor,
Manager,
ServerOwner,
}

View file

@ -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) {

View 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;
}

View 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;
}

View file

@ -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
View 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);
});

View 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);
}
}

View 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);
}
}