From 48c4b3578d98250d0b7b5c6224b2c354d31070b6 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 5 Sep 2021 14:34:06 +0300 Subject: [PATCH] Update server owner dashboard permissions automatically --- backend/src/data/AllowedGuilds.ts | 7 +- backend/src/data/ApiPermissionAssignments.ts | 67 +++++++++++++++++++ backend/src/data/apiAuditLogTypes.ts | 18 ++++- backend/src/data/entities/AllowedGuild.ts | 6 ++ backend/src/data/entities/ApiAuditLogEntry.ts | 2 +- .../data/entities/ApiPermissionAssignment.ts | 5 +- ...0840428694-AddTimestampsToAllowedGuilds.ts | 24 +++++++ .../GuildInfoSaver/GuildInfoSaverPlugin.ts | 35 ++++++---- backend/src/plugins/GuildInfoSaver/types.ts | 1 - 9 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 backend/src/migrations/1630840428694-AddTimestampsToAllowedGuilds.ts diff --git a/backend/src/data/AllowedGuilds.ts b/backend/src/data/AllowedGuilds.ts index e3399fea..747dbb87 100644 --- a/backend/src/data/AllowedGuilds.ts +++ b/backend/src/data/AllowedGuilds.ts @@ -2,6 +2,8 @@ import { getRepository, Repository } from "typeorm"; import { ApiPermissionTypes } from "./ApiPermissionAssignments"; import { BaseRepository } from "./BaseRepository"; import { AllowedGuild } from "./entities/AllowedGuild"; +import moment from "moment-timezone"; +import { DBDateFormat } from "../utils"; export class AllowedGuilds extends BaseRepository { private allowedGuilds: Repository; @@ -37,7 +39,10 @@ export class AllowedGuilds extends BaseRepository { } updateInfo(id, name, icon, ownerId) { - return this.allowedGuilds.update({ id }, { name, icon, owner_id: ownerId }); + return this.allowedGuilds.update( + { id }, + { name, icon, owner_id: ownerId, updated_at: moment.utc().format(DBDateFormat) }, + ); } add(id, data: Partial> = {}) { diff --git a/backend/src/data/ApiPermissionAssignments.ts b/backend/src/data/ApiPermissionAssignments.ts index fc04b61c..2497bf49 100644 --- a/backend/src/data/ApiPermissionAssignments.ts +++ b/backend/src/data/ApiPermissionAssignments.ts @@ -2,6 +2,9 @@ import { ApiPermissions } from "@shared/apiPermissions"; import { getRepository, Repository } from "typeorm"; import { BaseRepository } from "./BaseRepository"; import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment"; +import { Permissions } from "discord.js"; +import { ApiAuditLog } from "./ApiAuditLog"; +import { AuditLogEventTypes } from "./apiAuditLogTypes"; export enum ApiPermissionTypes { User = "USER", @@ -10,10 +13,12 @@ export enum ApiPermissionTypes { export class ApiPermissionAssignments extends BaseRepository { private apiPermissions: Repository; + private auditLogs: ApiAuditLog; constructor() { super(); this.apiPermissions = getRepository(ApiPermissionAssignment); + this.auditLogs = new ApiAuditLog(); } getByGuildId(guildId) { @@ -63,4 +68,66 @@ export class ApiPermissionAssignments extends BaseRepository { .andWhere("expires_at <= NOW()") .delete(); } + + async applyOwnerChange(guildId: string, newOwnerId: string) { + const existingPermissions = await this.getByGuildId(guildId); + let updatedOwner = false; + for (const perm of existingPermissions) { + let hasChanges = false; + + // Remove owner permission from anyone who currently has it + if (perm.permissions.includes(ApiPermissions.Owner)) { + perm.permissions.splice(perm.permissions.indexOf(ApiPermissions.Owner), 1); + hasChanges = true; + } + + // Add owner permission if we encounter the new owner + if (perm.type === ApiPermissionTypes.User && perm.target_id === newOwnerId) { + perm.permissions.push(ApiPermissions.Owner); + updatedOwner = true; + hasChanges = true; + } + + if (hasChanges) { + const criteria = { + guild_id: perm.guild_id, + type: perm.type, + target_id: perm.target_id, + }; + if (perm.permissions.length === 0) { + // No remaining permissions -> remove entry + this.auditLogs.addEntry(guildId, "0", AuditLogEventTypes.REMOVE_API_PERMISSION, { + type: perm.type, + target_id: perm.target_id, + }); + await this.apiPermissions.delete(criteria); + } else { + this.auditLogs.addEntry(guildId, "0", AuditLogEventTypes.EDIT_API_PERMISSION, { + type: perm.type, + target_id: perm.target_id, + permissions: perm.permissions, + expires_at: perm.expires_at, + }); + await this.apiPermissions.update(criteria, { + permissions: perm.permissions, + }); + } + } + } + + if (!updatedOwner) { + this.auditLogs.addEntry(guildId, "0", AuditLogEventTypes.ADD_API_PERMISSION, { + type: ApiPermissionTypes.User, + target_id: newOwnerId, + permissions: [ApiPermissions.Owner], + expires_at: null, + }); + await this.apiPermissions.insert({ + guild_id: guildId, + type: ApiPermissionTypes.User, + target_id: newOwnerId, + permissions: [ApiPermissions.Owner], + }); + } + } } diff --git a/backend/src/data/apiAuditLogTypes.ts b/backend/src/data/apiAuditLogTypes.ts index c50cc06d..8e9075af 100644 --- a/backend/src/data/apiAuditLogTypes.ts +++ b/backend/src/data/apiAuditLogTypes.ts @@ -1,7 +1,10 @@ +import { ApiPermissionTypes } from "./ApiPermissionAssignments"; + export const AuditLogEventTypes = { - ADD_API_PERMISSION: "ADD_API_PERMISSION", - REMOVE_API_PERMISSION: "REMOVE_API_PERMISSION", - EDIT_CONFIG: "EDIT_CONFIG", + ADD_API_PERMISSION: "ADD_API_PERMISSION" as const, + EDIT_API_PERMISSION: "EDIT_API_PERMISSION" as const, + REMOVE_API_PERMISSION: "REMOVE_API_PERMISSION" as const, + EDIT_CONFIG: "EDIT_CONFIG" as const, }; export type AuditLogEventType = keyof typeof AuditLogEventTypes; @@ -20,12 +23,21 @@ export type EditConfigEventData = {}; export interface AuditLogEventData extends Record { ADD_API_PERMISSION: { + type: ApiPermissionTypes; + target_id: string; + permissions: string[]; + expires_at: string | null; + }; + + EDIT_API_PERMISSION: { + type: ApiPermissionTypes; target_id: string; permissions: string[]; expires_at: string | null; }; REMOVE_API_PERMISSION: { + type: ApiPermissionTypes; target_id: string; }; diff --git a/backend/src/data/entities/AllowedGuild.ts b/backend/src/data/entities/AllowedGuild.ts index 129603ac..2ded9b44 100644 --- a/backend/src/data/entities/AllowedGuild.ts +++ b/backend/src/data/entities/AllowedGuild.ts @@ -14,4 +14,10 @@ export class AllowedGuild { @Column() owner_id: string; + + @Column() + created_at: string; + + @Column() + updated_at: string; } diff --git a/backend/src/data/entities/ApiAuditLogEntry.ts b/backend/src/data/entities/ApiAuditLogEntry.ts index 9c613353..0491c313 100644 --- a/backend/src/data/entities/ApiAuditLogEntry.ts +++ b/backend/src/data/entities/ApiAuditLogEntry.ts @@ -14,7 +14,7 @@ export class ApiAuditLogEntry { @Column() author_id: string; - @Column() + @Column({ type: String }) event_type: TEventType; @Column("simple-json") diff --git a/backend/src/data/entities/ApiPermissionAssignment.ts b/backend/src/data/entities/ApiPermissionAssignment.ts index 454fe17c..0171f940 100644 --- a/backend/src/data/entities/ApiPermissionAssignment.ts +++ b/backend/src/data/entities/ApiPermissionAssignment.ts @@ -1,5 +1,6 @@ import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm"; import { ApiUserInfo } from "./ApiUserInfo"; +import { ApiPermissionTypes } from "../ApiPermissionAssignments"; @Entity("api_permissions") export class ApiPermissionAssignment { @@ -7,9 +8,9 @@ export class ApiPermissionAssignment { @PrimaryColumn() guild_id: string; - @Column() + @Column({ type: "string" }) @PrimaryColumn() - type: string; + type: ApiPermissionTypes; @Column() @PrimaryColumn() diff --git a/backend/src/migrations/1630840428694-AddTimestampsToAllowedGuilds.ts b/backend/src/migrations/1630840428694-AddTimestampsToAllowedGuilds.ts new file mode 100644 index 00000000..8b29f669 --- /dev/null +++ b/backend/src/migrations/1630840428694-AddTimestampsToAllowedGuilds.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; + +export class AddTimestampsToAllowedGuilds1630840428694 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumns("allowed_guilds", [ + new TableColumn({ + name: "created_at", + type: "datetime", + default: "(NOW())", + }), + new TableColumn({ + name: "updated_at", + type: "datetime", + default: "(NOW())", + onUpdate: "CURRENT_TIMESTAMP", + }), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("allowed_guilds", "updated_at"); + await queryRunner.dropColumn("allowed_guilds", "created_at"); + } +} diff --git a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts index 3f241c65..880bd94e 100644 --- a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts +++ b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts @@ -1,9 +1,11 @@ import * as t from "io-ts"; -import { GuildPluginData } from "knub"; +import { GuildPluginData, typedGuildEventListener } from "knub"; import { AllowedGuilds } from "../../data/AllowedGuilds"; import { MINUTES } from "../../utils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { GuildInfoSaverPluginType } from "./types"; +import { Guild } from "discord.js"; +import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments"; export const GuildInfoSaverPlugin = zeppelinGuildPlugin()({ name: "guild_info_saver", @@ -11,13 +13,18 @@ export const GuildInfoSaverPlugin = zeppelinGuildPlugin updateGuildInfo(pluginData), 60 * MINUTES); + void updateGuildInfo(pluginData.guild); + pluginData.state.updateInterval = setInterval(() => updateGuildInfo(pluginData.guild), 60 * MINUTES); }, beforeUnload(pluginData) { @@ -25,11 +32,13 @@ export const GuildInfoSaverPlugin = zeppelinGuildPlugin) { - pluginData.state.allowedGuilds.updateInfo( - pluginData.guild.id, - pluginData.guild.name, - pluginData.guild.iconURL(), - pluginData.guild.ownerId, - ); +async function updateGuildInfo(guild: Guild) { + const allowedGuilds = new AllowedGuilds(); + const existingData = (await allowedGuilds.find(guild.id))!; + allowedGuilds.updateInfo(guild.id, guild.name, guild.iconURL(), guild.ownerId); + + if (existingData.owner_id !== guild.ownerId || existingData.created_at === existingData.updated_at) { + const apiPermissions = new ApiPermissionAssignments(); + apiPermissions.applyOwnerChange(guild.id, guild.ownerId); + } } diff --git a/backend/src/plugins/GuildInfoSaver/types.ts b/backend/src/plugins/GuildInfoSaver/types.ts index 25926bff..29013186 100644 --- a/backend/src/plugins/GuildInfoSaver/types.ts +++ b/backend/src/plugins/GuildInfoSaver/types.ts @@ -3,7 +3,6 @@ import { AllowedGuilds } from "../../data/AllowedGuilds"; export interface GuildInfoSaverPluginType extends BasePluginType { state: { - allowedGuilds: AllowedGuilds; updateInterval: NodeJS.Timeout; }; }