diff --git a/packages/client/src/components/Toolbar/UndoButton.tsx b/packages/client/src/components/Toolbar/UndoButton.tsx index 284dfe29ae7d1b05bfc383e0e5192e52cff828e6..a87b121e2f0db237b6850fbde3c8ffed72d25fdd 100644 --- a/packages/client/src/components/Toolbar/UndoButton.tsx +++ b/packages/client/src/components/Toolbar/UndoButton.tsx @@ -2,6 +2,7 @@ import { Button } from "@nextui-org/react"; import { useAppContext } from "../../contexts/AppContext"; import network from "../../lib/network"; import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; export const UndoButton = () => { const { undo, config } = useAppContext(); @@ -34,7 +35,21 @@ export const UndoButton = () => { // ref-ify this? function execUndo() { network.socket.emitWithAck("undo").then((data) => { - console.log("undo", data); + if (data.success) { + console.log("Undo pixel successful"); + } else { + console.log("Undo pixel error", data); + switch (data.error) { + case "pixel_covered": + toast.error("You cannot undo a covered pixel"); + break; + case "unavailable": + toast.error("You have no undo available"); + break; + default: + toast.error("Undo error: " + data.error); + } + } }); } diff --git a/packages/lib/src/net.ts b/packages/lib/src/net.ts index 8e765cb17447cc40e4779f2b9b3ef06bba18d38d..41e9bdccd8fc6700132681c329af590e40ac1f77 100644 --- a/packages/lib/src/net.ts +++ b/packages/lib/src/net.ts @@ -40,6 +40,7 @@ export interface ClientToServerEvents { ack: ( _: PacketAck< Pixel, + | "canvas_frozen" | "no_user" | "invalid_pixel" | "pixel_cooldown" @@ -50,7 +51,12 @@ export interface ClientToServerEvents { ) => void ) => void; undo: ( - ack: (_: PacketAck<{}, "no_user" | "unavailable" | "pixel_covered">) => void + ack: ( + _: PacketAck< + {}, + "canvas_frozen" | "no_user" | "unavailable" | "pixel_covered" + > + ) => void ) => void; subscribe: (topic: Subscription) => void; @@ -141,6 +147,7 @@ export type PalleteColor = { export type CanvasConfig = { size: [number, number]; + frozen: boolean; zoom: number; pixel: { maxStack: number; diff --git a/packages/server/prisma/dbml/schema.dbml b/packages/server/prisma/dbml/schema.dbml index d9987aafe76b84943d3dddc7032657dc1f32ab10..2336382a274851e846191268161989c36d029012 100644 --- a/packages/server/prisma/dbml/schema.dbml +++ b/packages/server/prisma/dbml/schema.dbml @@ -136,6 +136,8 @@ Enum AuditLogAction { BAN_DELETE CANVAS_SIZE CANVAS_FILL + CANVAS_FREEZE + CANVAS_UNFREEZE } Ref: Pixel.userId > User.sub diff --git a/packages/server/prisma/migrations/20240711173241_add_canvas_freeze_audit_log_type/migration.sql b/packages/server/prisma/migrations/20240711173241_add_canvas_freeze_audit_log_type/migration.sql new file mode 100644 index 0000000000000000000000000000000000000000..8a050a147ae18ec187e34ae6cee6394153905ab1 --- /dev/null +++ b/packages/server/prisma/migrations/20240711173241_add_canvas_freeze_audit_log_type/migration.sql @@ -0,0 +1,10 @@ +-- 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 'CANVAS_FREEZE'; +ALTER TYPE "AuditLogAction" ADD VALUE 'CANVAS_UNFREEZE'; diff --git a/packages/server/prisma/schema.prisma b/packages/server/prisma/schema.prisma index 308691fd458b354fa8fff4d3550e3ca0b9555a8d..d85cce5c235164d12528007c759008759d365fe1 100644 --- a/packages/server/prisma/schema.prisma +++ b/packages/server/prisma/schema.prisma @@ -154,6 +154,8 @@ enum AuditLogAction { BAN_DELETE CANVAS_SIZE CANVAS_FILL + CANVAS_FREEZE + CANVAS_UNFREEZE } model AuditLog { diff --git a/packages/server/src/api/admin.ts b/packages/server/src/api/admin.ts index 58503b80e18e10d25955e0c2bdf57faf0bf6185c..cd2ddac13d53bae17105f97c461353ac1fe28d0a 100644 --- a/packages/server/src/api/admin.ts +++ b/packages/server/src/api/admin.ts @@ -96,6 +96,49 @@ app.post("/canvas/size", async (req, res) => { res.send({ success: true, auditLog }); }); +/** + * 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); + + 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); + + 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(); diff --git a/packages/server/src/lib/Canvas.ts b/packages/server/src/lib/Canvas.ts index 6f7246b1c19bd2721ac29675669a4fa8d2a7bc82..cd42ea1eb506c0059216592c1fbe3966af0cdfff 100644 --- a/packages/server/src/lib/Canvas.ts +++ b/packages/server/src/lib/Canvas.ts @@ -13,14 +13,17 @@ class Canvas { * Size of the canvas */ private canvasSize: [width: number, height: number]; + private isFrozen: boolean; constructor() { this.canvasSize = [100, 100]; + this.isFrozen = false; } getCanvasConfig(): CanvasConfig { return { size: this.canvasSize, + frozen: this.isFrozen, zoom: 7, pixel: { cooldown: 10, @@ -33,6 +36,34 @@ class Canvas { }; } + get frozen() { + return this.isFrozen; + } + + async setFrozen(frozen: boolean) { + this.isFrozen = frozen; + + await prisma.setting.upsert({ + where: { key: "canvas.frozen" }, + create: { + key: "canvas.frozen", + value: JSON.stringify(frozen), + }, + update: { + key: "canvas.frozen", + value: JSON.stringify(frozen), + }, + }); + + if (SocketServer.instance) { + SocketServer.instance.broadcastConfig(); + } else { + Logger.warn( + "[Canvas#setFrozen] SocketServer is not instantiated, cannot broadcast config" + ); + } + } + /** * Change size of the canvas * diff --git a/packages/server/src/lib/Settings.ts b/packages/server/src/lib/Settings.ts index 4d1f595b6f5b07f3c78fb655d2be681e4898781d..cd5be45e82539c062e75f9cbd339dff6f288c798 100644 --- a/packages/server/src/lib/Settings.ts +++ b/packages/server/src/lib/Settings.ts @@ -26,6 +26,17 @@ export const loadSettings = async (frozen = false) => { Logger.warn("Setting canvas.size is not set, did you run init_settings?"); } + // canvas frozen + const canvasFrozen = await prisma.setting.findFirst({ + where: { key: "canvas.frozen" }, + }); + if (canvasFrozen) { + const data = JSON.parse(canvasFrozen.value); + Logger.info(`Canvas frozen loaded as ${data}`); + + Canvas.setFrozen(data); + } + Logger.info( "Settings loaded into memory, waiting for side effects to finish..." ); diff --git a/packages/server/src/lib/SocketServer.ts b/packages/server/src/lib/SocketServer.ts index 6163a8546a98e0a6600bf9a842e087bd0131b939..1099983c9015caece31a99ab81f29284f01a60c3 100644 --- a/packages/server/src/lib/SocketServer.ts +++ b/packages/server/src/lib/SocketServer.ts @@ -233,6 +233,11 @@ export class SocketServer { }); socket.on("place", async (pixel, bypassCooldown, ack) => { + if (getClientConfig().canvas.frozen) { + ack({ success: false, error: "canvas_frozen" }); + return; + } + if (!user) { ack({ success: false, error: "no_user" }); return; @@ -317,6 +322,11 @@ export class SocketServer { }); socket.on("undo", async (ack) => { + if (getClientConfig().canvas.frozen) { + ack({ success: false, error: "canvas_frozen" }); + return; + } + if (!user) { ack({ success: false, error: "no_user" }); return; diff --git a/packages/server/src/tools/init_settings.ts b/packages/server/src/tools/init_settings.ts index 2fe3b45bb754fe10490623ba614f943210376c45..b6afbb37d300e3340341fcae165d397ca51851d5 100644 --- a/packages/server/src/tools/init_settings.ts +++ b/packages/server/src/tools/init_settings.ts @@ -14,6 +14,10 @@ async function main() { height: 100, }, }, + { + key: "canvas.frozen", + defaultValue: false, + }, ]; for (const setting of SETTINGS) {