diff --git a/Dockerfile b/Dockerfile index b470299f41d2d4cf7a6b66ea37036e93a2af2b6b..c814cb25de3fa663d027a207ff8d08ed6052ebb6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -107,9 +107,13 @@ ENV PORT 3000 ENV NODE_ENV production ENV SERVE_CLIENT /home/node/app/packages/client ENV SERVE_ADMIN /home/node/app/packages/admin +ENV PIXEL_LOG_PATH /home/node/app/pixel.log + +VOLUME /home/node/app/pixel.log EXPOSE 3000 # profiler port, only used if profiler is explicity running EXPOSE 9229 + ENTRYPOINT [ "/bin/sh" ] CMD [ "./docker-start.sh" ] \ No newline at end of file diff --git a/packages/server/src/api/admin.ts b/packages/server/src/api/admin.ts index b3a75f5a91443edac35f5dadaba4ac8aa8ed2080..eb101a4296bd03f96cf296d12a4f6334ea058416 100644 --- a/packages/server/src/api/admin.ts +++ b/packages/server/src/api/admin.ts @@ -11,6 +11,7 @@ import { InstanceNotFound, } from "../models/Instance"; import { AuditLog } from "../models/AuditLog"; +import { LogMan } from "../lib/LogMan"; const app = Router(); const Logger = getLogger("HTTP/ADMIN"); @@ -50,6 +51,25 @@ app.get("/check", (req, res) => { res.send({ success: true }); }); +// 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"); +}); + app.get("/canvas/size", async (req, res) => { const config = Canvas.getCanvasConfig(); @@ -86,6 +106,11 @@ app.post("/canvas/size", async (req, res) => { } await Canvas.setSize(width, height); + + // 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 }); + const user = (await User.fromAuthSession(req.session.user!))!; const auditLog = AuditLog.Factory(user.sub) .doing("CANVAS_SIZE") @@ -111,6 +136,9 @@ app.get("/canvas/freeze", async (req, res) => { app.post("/canvas/freeze", async (req, res) => { await Canvas.setFrozen(true); + // same reason as canvas size changes, we log this here because #setFrozen is ran at startup + LogMan.log("canvas_freeze", {}); + const user = (await User.fromAuthSession(req.session.user!))!; const auditLog = AuditLog.Factory(user.sub) .doing("CANVAS_FREEZE") @@ -129,6 +157,9 @@ app.post("/canvas/freeze", async (req, res) => { app.delete("/canvas/freeze", async (req, res) => { await Canvas.setFrozen(false); + // same reason as canvas size changes, we log this here because #setFrozen is ran at startup + LogMan.log("canvas_unfreeze", {}); + const user = (await User.fromAuthSession(req.session.user!))!; const auditLog = AuditLog.Factory(user.sub) .doing("CANVAS_UNFREEZE") @@ -272,6 +303,13 @@ app.put("/canvas/undo", async (req, res) => { ? paletteColors.find((p) => p.hex === coveredPixel.color)?.id || -1 : -1, }); + + // 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, + }); break; } case "rejected": diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0a354d3fd7ee4119c3a84f9f2e1170d4cfbf4e12..7a9fcddc0e02e41b13b0cba6a98c0b948de7af98 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -81,6 +81,10 @@ if (!process.env.INHIBIT_LOGIN) { } } +if (!process.env.PIXEL_LOG_PATH) { + Logger.warn("PIXEL_LOG_PATH is not defined, defaulting to packages/server"); +} + // run startup tasks, all of these need to be completed to serve Promise.all([ Redis.getClient(), diff --git a/packages/server/src/lib/Canvas.ts b/packages/server/src/lib/Canvas.ts index d89af57f1c90123669b58152b6e5ace49a095fa4..9f0aaa71e6d040739698b6ea0881edf44a0052b8 100644 --- a/packages/server/src/lib/Canvas.ts +++ b/packages/server/src/lib/Canvas.ts @@ -5,6 +5,7 @@ import { SocketServer } from "./SocketServer"; import { getLogger } from "./Logger"; import { Pixel } from "@prisma/client"; import { CanvasWorker } from "../workers/worker"; +import { LogMan } from "./LogMan"; const Logger = getLogger("CANVAS"); @@ -182,7 +183,7 @@ class Canvas { x: pixel.x, y: pixel.y, createdAt: { lt: pixel.createdAt }, - deletedAt: null, + deletedAt: null, // undone pixels will have this set }, orderBy: { createdAt: "desc" }, take: 1, @@ -198,6 +199,11 @@ class Canvas { }); } + LogMan.log("pixel_undo", pixel.userId, { + x: pixel.x, + y: pixel.y, + hex: coveringPixel?.color, + }); return coveringPixel; } @@ -392,6 +398,8 @@ class Canvas { hex, })) ); + + LogMan.log("mod_fill", user.sub, { from: start, to: end, hex }); } async setPixel( @@ -430,6 +438,11 @@ class Canvas { await this.updateCanvasRedisAtPos(x, y); Logger.info(`${user.sub} placed pixel at (${x}, ${y})`); + LogMan.log(isModAction ? "mod_override" : "pixel_place", user.sub, { + x, + y, + hex, + }); } /** diff --git a/packages/server/src/lib/LogMan.ts b/packages/server/src/lib/LogMan.ts new file mode 100644 index 0000000000000000000000000000000000000000..3bfec0f26ceee52d48ebdf9edcb5d457bc56b6c0 --- /dev/null +++ b/packages/server/src/lib/LogMan.ts @@ -0,0 +1,85 @@ +import { PixelLogger } from "./Logger"; + +interface UserEvents { + pixel_place: { x: number; y: number; hex: string }; + pixel_undo: { x: number; y: number; hex?: string }; + mod_fill: { + from: [x: number, y: number]; + to: [x: number, y: number]; + hex: string; + }; + mod_override: { x: number; y: number; hex: string }; + mod_rollback: { x: number; y: number; hex?: string }; + mod_rollback_undo: { x: number; y: number; hex?: string }; +} + +interface SystemEvents { + canvas_size: { width: number; height: number }; + canvas_freeze: {}; + canvas_unfreeze: {}; +} + +/** + * Handle logs that should be written to a text file + * + * This could be used as an EventEmitter in the future, but as of right now + * it just adds typing to logging of these events + * + * TODO: better name, this one is not it + * + * @see #57 + */ +class LogMan_ { + log( + event: EventName, + data: SystemEvents[EventName] + ): void; + log( + event: EventName, + user: string, + data: UserEvents[EventName] + ): void; + log( + event: EventName, + ...params: EventName extends keyof UserEvents + ? [user: string, data: UserEvents[EventName]] + : EventName extends keyof SystemEvents + ? [data: SystemEvents[EventName]] + : never + ): void { + let parts: string[] = []; + + if (params.length === 2) { + // user event + let user = params[0] as string; + parts.push(user, event); + + if (event === "mod_fill") { + // this event format has a different line format + let data: UserEvents["mod_fill"] = params[1] as any; + + parts.push(data.from.join(","), data.to.join(","), data.hex); + } else { + let data: UserEvents[Exclude] = + params[1] as any; + parts.push(...[data.x, data.y, data.hex || "unset"].map((a) => a + "")); + } + } else { + // system event + + parts.push("system", event); + + switch (event) { + case "canvas_size": + let data: SystemEvents["canvas_size"] = params[0] as any; + let { width, height } = data; + parts.push(width + "", height + ""); + break; + } + } + + PixelLogger.info(parts.join("\t")); + } +} + +export const LogMan = new LogMan_(); diff --git a/packages/server/src/lib/Logger.ts b/packages/server/src/lib/Logger.ts index f2ee3d2572b5d5810c39ceb1b9b78bb486380007..ec9261a5ca9a16c30567cb75d0450abc212472eb 100644 --- a/packages/server/src/lib/Logger.ts +++ b/packages/server/src/lib/Logger.ts @@ -1,6 +1,11 @@ import winston, { format } from "winston"; +import path from "node:path"; import { createEnum } from "./utils"; +// if PIXEL_LOG_PATH is defined, use that, otherwise default to packages/server root +const PIXEL_LOG_PATH = + process.env.PIXEL_LOG_PATH || path.join(__dirname, "..", "..", "pixels.log"); + const formatter = format.printf((options) => { let maxModuleWidth = 0; for (const module of Object.values(LoggerType)) { @@ -26,6 +31,18 @@ const Winston = winston.createLogger({ transports: [new winston.transports.Console()], }); +// Used by LogMan for writing to pixels.log +export const PixelLogger = winston.createLogger({ + format: format.printf((options) => { + return [new Date().toISOString(), options.message].join("\t"); + }), + transports: [ + new winston.transports.File({ + filename: PIXEL_LOG_PATH, + }), + ], +}); + export const LoggerType = createEnum([ "MAIN", "SETTINGS", diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 727a6e7ed97523087ab2cf8251b51a3875751341..e7f7c3158c16a36fb79e4926ece023f0071445b3 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -61,6 +61,8 @@ declare global { ELEMENT_HOST: string; MATRIX_GENERAL_ALIAS: string; + PIXEL_LOG_PATH?: string; + RECAPTCHA_SITE_KEY?: string; RECAPTCHA_SECRET_KEY?: string; RECAPTCHA_PIXEL_CHANCE?: string;