Skip to content
admin.ts 27.1 KiB
Newer Older
Grant's avatar
Grant committed
import { Router } from "express";
Grant's avatar
Grant committed
import { User, UserNotBanned, UserNotFound } from "../models/User";
Grant's avatar
Grant committed
import Canvas from "../lib/Canvas";
Grant's avatar
Grant committed
import { getLogger } from "../lib/Logger";
import { RateLimiter } from "../lib/RateLimiter";
Grant's avatar
Grant committed
import { prisma } from "../lib/prisma";
import { SocketServer } from "../lib/SocketServer";
import {
  Instance,
  InstanceNotBanned,
  InstanceNotFound,
} from "../models/Instance";
Grant's avatar
Grant committed
import { AuditLog } from "../models/AuditLog";
Grant's avatar
Grant committed
import { LogMan } from "../lib/LogMan";
Grant's avatar
Grant committed

const app = Router();
Grant's avatar
Grant committed
const Logger = getLogger("HTTP/ADMIN");
app.use(RateLimiter.ADMIN);

Grant's avatar
Grant committed
app.use(async (req, res, next) => {
  if (!req.session.user) {
    res.status(401).json({
      success: false,
      error: "You are not logged in",
    });
    return;
  }

  const user = await User.fromAuthSession(req.session.user);
  if (!user) {
    res.status(400).json({
      success: false,
      error: "User data does not exist?",
    });
    return;
  }

  if (!user.isAdmin) {
    res.status(403).json({
      success: false,
      error: "user is not admin",
    });
    return;
  }

  next();
});

app.get("/check", (req, res) => {
  res.send({ success: true });
});

Grant's avatar
Grant committed
// TODO: Delete before merge
app.get("/log", (req, res) => {
  const user = "grant@grants.cafe";

  for (let i = 0; i < 100; i++) {
    LogMan.log("pixel_place", user, { x: 0, y: 0, hex: "ABC123" });
    LogMan.log("pixel_undo", user, { x: 0, y: 0, hex: "FFFFFF" });
    LogMan.log("mod_fill", user, { from: [0, 0], to: [1, 1], hex: "000000" });
    LogMan.log("mod_override", user, { x: 0, y: 0, hex: "111111" });
    LogMan.log("mod_rollback", user, { x: 0, y: 0, hex: "222222" });
    LogMan.log("mod_rollback_undo", user, { x: 0, y: 0, hex: "333333" });
    LogMan.log("canvas_size", { width: 100, height: 100 });
    LogMan.log("canvas_freeze", {});
    LogMan.log("canvas_unfreeze", {});
  }

  res.send("ok");
});

Grant's avatar
Grant committed
app.get("/canvas/size", async (req, res) => {
  const config = Canvas.getCanvasConfig();

  res.json({
    success: true,
    size: {
      width: config.size[0],
      height: config.size[1],
    },
  });
});

Grant's avatar
Grant committed
/**
 * Update canvas size
 *
 * @header X-Audit
 * @body width number
 * @body height number
 */
Grant's avatar
Grant committed
app.post("/canvas/size", async (req, res) => {
  const width = parseInt(req.body.width || "-1");
  const height = parseInt(req.body.height || "-1");

  if (
    isNaN(width) ||
    isNaN(height) ||
    width < 1 ||
    height < 1 ||
    width > 10000 ||
    height > 10000
  ) {
    res.status(400).json({ success: false, error: "what are you doing" });
    return;
  }

  await Canvas.setSize(width, height);
Grant's avatar
Grant committed

  // we log this here because Canvas#setSize is ran at launch
  // this is currently the only way the size is changed is via the API
  LogMan.log("canvas_size", { width, height });

Grant's avatar
Grant committed
  const user = (await User.fromAuthSession(req.session.user!))!;
  const auditLog = AuditLog.Factory(user.sub)
    .doing("CANVAS_SIZE")
    .reason(req.header("X-Audit") || null)
    .withComment(`Changed canvas size to ${width}x${height}`)
    .create();
Grant's avatar
Grant committed
  res.send({ success: true, auditLog });
Grant's avatar
Grant committed
/**
 * Get canvas frozen status
 */
app.get("/canvas/freeze", async (req, res) => {
  res.send({ success: true, frozen: Canvas.frozen });
});

/**
 * Freeze the canvas
 *
 * @header X-Audit
 */
app.post("/canvas/freeze", async (req, res) => {
  await Canvas.setFrozen(true);

Grant's avatar
Grant committed
  // same reason as canvas size changes, we log this here because #setFrozen is ran at startup
  LogMan.log("canvas_freeze", {});

Grant's avatar
Grant committed
  const user = (await User.fromAuthSession(req.session.user!))!;
  const auditLog = AuditLog.Factory(user.sub)
    .doing("CANVAS_FREEZE")
    .reason(req.header("X-Audit") || null)
    .withComment(`Freezed the canvas`)
    .create();

  res.send({ success: true, auditLog });
});

/**
 * Unfreeze the canvas
 *
 * @header X-Audit
 */
app.delete("/canvas/freeze", async (req, res) => {
  await Canvas.setFrozen(false);

Grant's avatar
Grant committed
  // same reason as canvas size changes, we log this here because #setFrozen is ran at startup
  LogMan.log("canvas_unfreeze", {});

Grant's avatar
Grant committed
  const user = (await User.fromAuthSession(req.session.user!))!;
  const auditLog = AuditLog.Factory(user.sub)
    .doing("CANVAS_UNFREEZE")
    .reason(req.header("X-Audit") || null)
    .withComment(`Un-Freezed the canvas`)
    .create();

  res.send({ success: true, auditLog });
});

app.put("/canvas/heatmap", async (req, res) => {
  try {
    await Canvas.generateHeatmap();

    res.send({ success: true });
  } catch (e) {
    Logger.error(e);
    res.send({ success: false, error: "Failed to generate" });
  }
});

Grant's avatar
Grant committed
app.post("/canvas/forceUpdateTop", async (req, res) => {
  Logger.info("Starting force updating isTop");

  await Canvas.forceUpdatePixelIsTop();

  Logger.info("Finished force updating isTop");
  res.send({ success: true });
});

app.get("/canvas/:x/:y", async (req, res) => {
  const x = parseInt(req.params.x);
  const y = parseInt(req.params.y);

  res.json(await Canvas.getPixel(x, y));
});

app.post("/canvas/stress", async (req, res) => {
  if (
    typeof req.body?.width !== "number" ||
    typeof req.body?.height !== "number"
  ) {
    res.status(400).json({ success: false, error: "width/height is invalid" });
    return;
  }

Grant's avatar
Grant committed
  const style: "random" | "xygradient" = req.body.style || "random";

Grant's avatar
Grant committed
  const width: number = req.body.width;
  const height: number = req.body.height;
Grant's avatar
Grant committed
  const user = (await User.fromAuthSession(req.session.user!))!;
  const paletteColors = await prisma.paletteColor.findMany({});

  let promises: Promise<any>[] = [];
Grant's avatar
Grant committed

  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
Grant's avatar
Grant committed
      promises.push(
        new Promise(async (res) => {
          let colorIndex: number;
          if (style === "xygradient") {
            colorIndex =
              Math.floor((x / width) * (paletteColors.length / 2)) +
              Math.floor((y / height) * (paletteColors.length / 2));
          } else {
            colorIndex = Math.floor(Math.random() * paletteColors.length);
          }

          let color = paletteColors[colorIndex];

          await Canvas.setPixel(user, x, y, color.hex, false);

          SocketServer.instance.io.emit("pixel", {
            x,
            y,
            color: color.id,
          });
          res(undefined);
        })
      );
Grant's avatar
Grant committed
  await Promise.allSettled(promises);
Grant's avatar
Grant committed
  res.send("ok");
});

Grant's avatar
Grant committed
/**
 * Undo a square
 *
 * @header X-Audit
 * @body start.x number
 * @body start.y number
 * @body end.x number
 * @body end.y number
 */
app.put("/canvas/undo", async (req, res) => {
  if (
    typeof req.body?.start?.x !== "number" ||
    typeof req.body?.start?.y !== "number"
  ) {
    res
      .status(400)
      .json({ success: false, error: "start position is invalid" });
    return;
  }

  if (
    typeof req.body?.end?.x !== "number" ||
    typeof req.body?.end?.y !== "number"
  ) {
    res.status(400).json({ success: false, error: "end position is invalid" });
    return;
  }

  const user_sub =
    req.session.user!.user.username +
    "@" +
    req.session.user!.service.instance.hostname;
  const start_position: [x: number, y: number] = [
    req.body.start.x,
    req.body.start.y,
  ];
  const end_position: [x: number, y: number] = [req.body.end.x, req.body.end.y];

  const width = end_position[0] - start_position[0];
  const height = end_position[1] - start_position[1];

  const pixels = await Canvas.undoArea(start_position, end_position);
  const paletteColors = await prisma.paletteColor.findMany({});

  for (const pixel of pixels) {
    switch (pixel.status) {
      case "fulfilled": {
        const coveredPixel = pixel.value;

        SocketServer.instance.io.emit("pixel", {
          x: pixel.pixel.x,
          y: pixel.pixel.y,
          color: coveredPixel
            ? paletteColors.find((p) => p.hex === coveredPixel.color)?.id || -1
            : -1,
        });
Grant's avatar
Grant committed

        // TODO: this spams the log, it would be nicer if it combined
        LogMan.log("mod_rollback", user_sub, {
          x: pixel.pixel.x,
          y: pixel.pixel.y,
          hex: coveredPixel?.color,
        });
Grant's avatar
Grant committed
        break;
      }
      case "rejected":
        console.log("Failed to undo pixel", pixel);
        break;
    }
  }

  const user = (await User.fromAuthSession(req.session.user!))!;
  const auditLog = await AuditLog.Factory(user.sub)
    .doing("CANVAS_AREA_UNDO")
    .reason(req.header("X-Audit") || null)
    .withComment(
      `Area undo (${start_position.join(",")}) -> (${end_position.join(",")})`
    )
    .create();

  res.json({ success: true, auditLog });
});

Grant's avatar
Grant committed
/**
 * Fill an area
Grant's avatar
Grant committed
 *
 * @header X-Audit
 * @body start.x number
 * @body start.y number
 * @body end.x number
 * @body end.y number
 * @body color number Palette color index
Grant's avatar
Grant committed
 */
app.put("/canvas/fill", async (req, res) => {
  if (
    typeof req.body?.start?.x !== "number" ||
    typeof req.body?.start?.y !== "number"
  ) {
    res
      .status(400)
      .json({ success: false, error: "start position is invalid" });
    return;
  }

  if (
    typeof req.body?.end?.x !== "number" ||
    typeof req.body?.end?.y !== "number"
  ) {
    res.status(400).json({ success: false, error: "end position is invalid" });
    return;
  }

  if (typeof req.body.color !== "number") {
    res.status(400).json({ success: false, error: "color is invalid" });
    return;
  }

  const user_sub =
    req.session.user!.user.username +
    "@" +
    req.session.user!.service.instance.hostname;
  const start_position: [x: number, y: number] = [
    req.body.start.x,
    req.body.start.y,
  ];
  const end_position: [x: number, y: number] = [req.body.end.x, req.body.end.y];
  const palette = await prisma.paletteColor.findFirst({
    where: { id: req.body.color },
  });

  if (!palette) {
    res.status(400).json({ success: false, error: "invalid color" });
    return;
  }

  const width = end_position[0] - start_position[0];
  const height = end_position[1] - start_position[1];
  const area = width * height;

  // if (area > 50 * 50) {
  //   res.status(400).json({ success: false, error: "Area too big" });
  //   return;
  // }

  await Canvas.fillArea(
    { sub: user_sub },
    start_position,
    end_position,
    palette.hex
  );

  SocketServer.instance.io.emit(
    "square",
    start_position,
    end_position,
    palette.id
  );

Grant's avatar
Grant committed
  const user = (await User.fromAuthSession(req.session.user!))!;
  const auditLog = await AuditLog.Factory(user.sub)
    .doing("CANVAS_FILL")
    .reason(req.header("X-Audit") || null)
    .withComment(
      `Filled (${start_position.join(",")}) -> (${end_position.join(",")}) with ${palette.hex}`
    )
    .create();

  res.json({ success: true, auditLog });
/**
 * Get ip address info
 *
 * @query address IP address
 */
app.get("/ip", async (req, res) => {
  if (typeof req.query.address !== "string") {
    return res.status(400).json({ success: false, error: "missing ?address=" });
  }

  const ip: string = req.query.address;

  const results = await prisma.iPAddress.findMany({
    select: {
      userSub: true,
      createdAt: true,
      lastUsedAt: true,
    },
    where: {
      ip,
    },
  });

  res.json({ success: true, results });
});

/**
 * Get all of a user's IP addresses
 *
 * @param :sub User ID
 */
app.get("/user/:sub/ips", 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 {
      Logger.error(`/user/${req.params.sub}/ips Error ` + (e as any)?.message);
      res.status(500).json({ success: false, error: "Internal error" });
    }
    return;
  }

  const ips = await prisma.iPAddress.findMany({
    where: {
      userSub: user.sub,
    },
  });

  res.json({ success: true, ips });
});

Grant's avatar
Grant committed
/**
 * Create or ban a user
 *
 * @header X-Audit
 * @param :sub User sub claim
 * @body expiresAt string! ISO date time string
 * @body publicNote string?
 * @body privateNote string?
 */
app.put("/user/:sub/ban", async (req, res) => {
  let user: User;
  let expires: Date;
  let publicNote: string | undefined | null;
  let privateNote: string | undefined | null;

  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 {
      Logger.error(`/user/${req.params.sub}/ban Error ` + (e as any)?.message);
      res.status(500).json({ success: false, error: "Internal error" });
    }
    return;
  }

  if (typeof req.body.expiresAt !== "string") {
    res
      .status(400)
      .json({ success: false, error: "expiresAt is not a string" });
    return;
  }

  expires = new Date(req.body.expiresAt);

  if (!isFinite(expires.getTime())) {
    res
      .status(400)
      .json({ success: false, error: "expiresAt is not a valid date" });
    return;
  }

  if (typeof req.body.publicNote !== "undefined") {
    if (
      typeof req.body.publicNote !== "string" &&
      req.body.privateNote !== null
    ) {
      res.status(400).json({
        success: false,
        error: "publicNote is set and is not a string",
      });
      return;
    }

    publicNote = req.body.publicNote;
  }

  if (typeof req.body.privateNote !== "undefined") {
    if (
      typeof req.body.privateNote !== "string" &&
      req.body.privateNote !== null
    ) {
      res.status(400).json({
        success: false,
        error: "privateNote is set and is not a string",
      });
      return;
    }

    privateNote = req.body.privateNote;
  }

Grant's avatar
Grant committed
  const existingBan = user.getBan();
  const ban = await user.ban(expires, publicNote, privateNote);

  let shouldNotifyUser = false;

  if (existingBan) {
    if (existingBan.expires.getTime() !== ban.expiresAt.getTime()) {
      shouldNotifyUser = true;
    }
  } else {
    shouldNotifyUser = true;
  }

  if (shouldNotifyUser) {
    user.notify({
      is: "modal",
      action: "moderation",
      dismissable: true,
      message_key: "banned",
      metadata: {
        until: expires.toISOString(),
      },
    });
  }

  user.updateStanding();

Grant's avatar
Grant committed
  const adminUser = (await User.fromAuthSession(req.session.user!))!;
Grant's avatar
Grant committed
  const auditLog = await AuditLog.Factory(adminUser.sub)
Grant's avatar
Grant committed
    .doing(existingBan ? "BAN_UPDATE" : "BAN_CREATE")
    .reason(req.header("X-Audit") || null)
    .withComment(
      existingBan
        ? `Updated ban on ${user.sub}`
        : `Created a ban for ${user.sub}`
    )
    .withBan(ban)
    .create();

Grant's avatar
Grant committed
  res.json({ success: true, auditLog });
Grant's avatar
Grant committed
/**
 * Delete a user ban
 *
 * @header X-Audit
 * @param :sub User sub
 */
app.delete("/user/:sub/ban", async (req, res) => {
  // delete ban ("unban")

  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 {
      Logger.error(`/user/${req.params.sub}/ban Error ` + (e as any)?.message);
      res.status(500).json({ success: false, error: "Internal error" });
    }
    return;
  }

Grant's avatar
Grant committed
  try {
    await user.unban();
  } catch (e) {
    if (e instanceof UserNotBanned) {
      res.status(404).json({ success: false, error: "User is not banned" });
    } else {
      Logger.error(
        `/instance/${req.params.sub}/ban Error ` + (e as any)?.message
      );
      res.status(500).json({ success: false, error: "Internal error" });
    }
    return;
  }

  user.notify({
    is: "modal",
    action: "moderation",
    dismissable: true,
    message_key: "unbanned",
    metadata: {},
  });

  await user.update(true);
  user.updateStanding();

Grant's avatar
Grant committed
  const adminUser = (await User.fromAuthSession(req.session.user!))!;
Grant's avatar
Grant committed
  const auditLog = await AuditLog.Factory(adminUser.sub)
Grant's avatar
Grant committed
    .doing("BAN_DELETE")
    .reason(req.header("X-Audit") || null)
    .withComment(`Deleted ban for ${user.sub}`)
    .create();
Grant's avatar
Grant committed
  res.json({ success: true, auditLog });
Grant's avatar
Grant committed
/**
 * Send notice to every connected socket
 *
 * @body title string
 * @body body string?
 */
app.post("/user/all/notice", async (req, res) => {
  let title: string = req.body.title;

  if (typeof req.body.title !== "string") {
    return res
      .status(400)
      .json({ success: false, error: "Title is not a string" });
  }

  if (
    typeof req.body.body !== "undefined" &&
    typeof req.body.body !== "string"
  ) {
    return res
      .status(400)
      .json({ success: false, error: "Body is set but is not a string" });
  }

  const sockets = await SocketServer.instance.io.fetchSockets();

  for (const socket of sockets) {
    socket.emit("alert", {
      is: "modal",
      action: "moderation",
      dismissable: true,
      title,
      body: req.body.body,
    });
  }

  res.json({ success: true });
});

/**
 * Send notice to user
 *
 * @param :sub User id
 * @body title string
 * @body body string?
 */
app.post("/user/:sub/notice", 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 {
      Logger.error(`/user/${req.params.sub}/ban Error ` + (e as any)?.message);
      res.status(500).json({ success: false, error: "Internal error" });
    }
    return;
  }

  let title: string = req.body.title;

  if (typeof req.body.title !== "string") {
    return res
      .status(400)
      .json({ success: false, error: "Title is not a string" });
  }

  if (
    typeof req.body.body !== "undefined" &&
    typeof req.body.body !== "string"
  ) {
    return res
      .status(400)
      .json({ success: false, error: "Body is set but is not a string" });
  }

  user.notify({
    is: "modal",
    action: "moderation",
    dismissable: true,
    title,
    body: req.body.body,
  });

  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

  let instance: Instance;

  try {
    instance = await Instance.fromDomain(req.params.domain);
  } catch (e) {
    if (e instanceof InstanceNotFound) {
      res.status(404).json({ success: false, error: "instance not found" });
    } else {
      Logger.error(
        `/instance/${req.params.domain}/ban Error ` + (e as any)?.message
      );
      res.status(500).json({ success: false, error: "Internal error" });
    }
    return;
  }

  const ban = await instance.getEffectiveBan();

  if (!ban) {
    return res
      .status(404)
      .json({ success: false, error: "Instance not banned" });
  }

  res.json({ success: true, ban });
});

Grant's avatar
Grant committed
/**
 * Create or update a ban for an instance (and subdomains)
 *
 * @header X-Audit
 * @param :domain Domain for the instance
 * @body expiresAt string! ISO date time string
 * @body publicNote string?
 * @body privateNote string?
 */
app.put("/instance/:domain/ban", async (req, res) => {
  // ban domain & subdomains

  let instance: Instance;
  let expires: Date;
  let publicNote: string | null | undefined;
  let privateNote: string | null | undefined;

  try {
    instance = await Instance.fromDomain(req.params.domain);
  } catch (e) {
    if (e instanceof InstanceNotFound) {
      res.status(404).json({ success: false, error: "instance not found" });
    } else {
      Logger.error(
        `/instance/${req.params.domain}/ban Error ` + (e as any)?.message
      );
      res.status(500).json({ success: false, error: "Internal error" });
    }
    return;
  }

  if (typeof req.body.expiresAt !== "string") {
    res
      .status(400)
      .json({ success: false, error: "expiresAt is not a string" });
    return;
  }

  expires = new Date(req.body.expiresAt);

  if (!isFinite(expires.getTime())) {
    res
      .status(400)
      .json({ success: false, error: "expiresAt is not a valid date" });
    return;
  }

  if (typeof req.body.publicNote !== "undefined") {
    if (
      typeof req.body.publicNote !== "string" &&
      req.body.privateNote !== null
    ) {
      res.status(400).json({
        success: false,
        error: "publicNote is set and is not a string",
      });
      return;
    }

    publicNote = req.body.publicNote;
  }

  if (typeof req.body.privateNote !== "undefined") {
    if (
      typeof req.body.privateNote !== "string" &&
      req.body.privateNote !== null
    ) {
      res.status(400).json({
        success: false,
        error: "privateNote is set and is not a string",
      });
      return;
    }

    privateNote = req.body.privateNote;
  }

Grant's avatar
Grant committed
  const hasExistingBan = await instance.getBan();

  const user = (await User.fromAuthSession(req.session.user!))!;
  const ban = await instance.ban(expires, publicNote, privateNote);
Grant's avatar
Grant committed
  const auditLog = await AuditLog.Factory(user.sub)