From 013f5b8bb51a3bce8a1d41b10053661789c72026 Mon Sep 17 00:00:00 2001 From: Grant Date: Sat, 13 Jul 2024 10:02:23 -0600 Subject: [PATCH] [admin] Add moderation/admin management endpoints --- packages/server/prisma/dbml/schema.dbml | 4 + .../migration.sql | 12 ++ packages/server/prisma/schema.prisma | 8 +- packages/server/src/api/admin.ts | 152 ++++++++++++++++++ 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 packages/server/prisma/migrations/20240713155333_add_mod_and_admin_audit_log_actions/migration.sql diff --git a/packages/server/prisma/dbml/schema.dbml b/packages/server/prisma/dbml/schema.dbml index f080050..aa417a3 100644 --- a/packages/server/prisma/dbml/schema.dbml +++ b/packages/server/prisma/dbml/schema.dbml @@ -139,6 +139,10 @@ Enum AuditLogAction { CANVAS_FREEZE CANVAS_UNFREEZE CANVAS_AREA_UNDO + USER_MOD + USER_UNMOD + USER_ADMIN + USER_UNADMIN } Ref: Pixel.userId > User.sub diff --git a/packages/server/prisma/migrations/20240713155333_add_mod_and_admin_audit_log_actions/migration.sql b/packages/server/prisma/migrations/20240713155333_add_mod_and_admin_audit_log_actions/migration.sql new file mode 100644 index 0000000..8bfd047 --- /dev/null +++ b/packages/server/prisma/migrations/20240713155333_add_mod_and_admin_audit_log_actions/migration.sql @@ -0,0 +1,12 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "AuditLogAction" ADD VALUE 'USER_MOD'; +ALTER TYPE "AuditLogAction" ADD VALUE 'USER_UNMOD'; +ALTER TYPE "AuditLogAction" ADD VALUE 'USER_ADMIN'; +ALTER TYPE "AuditLogAction" ADD VALUE 'USER_UNADMIN'; diff --git a/packages/server/prisma/schema.prisma b/packages/server/prisma/schema.prisma index 75a99e8..9df5391 100644 --- a/packages/server/prisma/schema.prisma +++ b/packages/server/prisma/schema.prisma @@ -26,8 +26,8 @@ model User { profile_url String? lastTimeGainStarted DateTime @default(now()) // the time base used to determine the amount of stack the user should gain - pixelStack Int @default(0) // amount of pixels stacked for this user - undoExpires DateTime? // when the undo for the most recent pixel expires at + pixelStack Int @default(0) // amount of pixels stacked for this user + undoExpires DateTime? // when the undo for the most recent pixel expires at isAdmin Boolean @default(false) isModerator Boolean @default(false) @@ -157,6 +157,10 @@ enum AuditLogAction { CANVAS_FREEZE CANVAS_UNFREEZE CANVAS_AREA_UNDO + USER_MOD + USER_UNMOD + USER_ADMIN + USER_UNADMIN } model AuditLog { diff --git a/packages/server/src/api/admin.ts b/packages/server/src/api/admin.ts index f3a2661..26f4af5 100644 --- a/packages/server/src/api/admin.ts +++ b/packages/server/src/api/admin.ts @@ -679,6 +679,158 @@ app.post("/user/:sub/notice", async (req, res) => { res.json({ success: true }); }); +/** + * Mark a user as a moderator + * + * @param :sub User ID + */ +app.put("/user/:sub/moderator", async (req, res) => { + let user: User; + + try { + user = await User.fromSub(req.params.sub); + } catch (e) { + if (e instanceof UserNotFound) { + res.status(404).json({ success: false, error: "User not found" }); + } else { + res.status(500).json({ success: false, error: "Internal error" }); + } + return; + } + + await prisma.user.update({ + where: { sub: user.sub }, + data: { + isModerator: true, + }, + }); + + await user.update(true); + + const adminUser = (await User.fromAuthSession(req.session.user!))!; + const auditLog = await AuditLog.Factory(adminUser.sub) + .doing("USER_MOD") + .reason(req.header("X-Audit") || null) + .withComment(`Made ${user.sub} a moderator`) + .create(); + + res.json({ success: true, auditLog }); +}); + +/** + * Unmark a user as a moderator + * + * @param :sub User ID + */ +app.delete("/user/:sub/moderator", async (req, res) => { + let user: User; + + try { + user = await User.fromSub(req.params.sub); + } catch (e) { + if (e instanceof UserNotFound) { + res.status(404).json({ success: false, error: "User not found" }); + } else { + res.status(500).json({ success: false, error: "Internal error" }); + } + return; + } + + await prisma.user.update({ + where: { sub: user.sub }, + data: { + isModerator: false, + }, + }); + + await user.update(true); + + const adminUser = (await User.fromAuthSession(req.session.user!))!; + const auditLog = await AuditLog.Factory(adminUser.sub) + .doing("USER_UNMOD") + .reason(req.header("X-Audit") || null) + .withComment(`Removed ${user.sub} as moderator`) + .create(); + + res.json({ success: true, auditLog }); +}); + +/** + * Mark a user as an admin + * + * @param :sub User ID + */ +app.put("/user/:sub/admin", async (req, res) => { + let user: User; + + try { + user = await User.fromSub(req.params.sub); + } catch (e) { + if (e instanceof UserNotFound) { + res.status(404).json({ success: false, error: "User not found" }); + } else { + res.status(500).json({ success: false, error: "Internal error" }); + } + return; + } + + await prisma.user.update({ + where: { sub: user.sub }, + data: { + isAdmin: true, + }, + }); + + await user.update(true); + + const adminUser = (await User.fromAuthSession(req.session.user!))!; + const auditLog = await AuditLog.Factory(adminUser.sub) + .doing("USER_ADMIN") + .reason(req.header("X-Audit") || null) + .withComment(`Added ${user.sub} as admin`) + .create(); + + res.json({ success: true, auditLog }); +}); + +/** + * Unmark a user as an admin + * + * @param :sub User ID + */ +app.delete("/user/:sub/admin", async (req, res) => { + let user: User; + + try { + user = await User.fromSub(req.params.sub); + } catch (e) { + if (e instanceof UserNotFound) { + res.status(404).json({ success: false, error: "User not found" }); + } else { + res.status(500).json({ success: false, error: "Internal error" }); + } + return; + } + + await prisma.user.update({ + where: { sub: user.sub }, + data: { + isAdmin: false, + }, + }); + + await user.update(true); + + const adminUser = (await User.fromAuthSession(req.session.user!))!; + const auditLog = await AuditLog.Factory(adminUser.sub) + .doing("USER_UNADMIN") + .reason(req.header("X-Audit") || null) + .withComment(`Removed ${user.sub} as admin`) + .create(); + + res.json({ success: true, auditLog }); +}); + app.get("/instance/:domain/ban", async (req, res) => { // get ban information