Merge branch 'main' of ssh://git.sc07.company:2424/sc07/canvas
This commit is contained in:
commit
217953af35
|
@ -107,9 +107,13 @@ ENV PORT 3000
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV SERVE_CLIENT /home/node/app/packages/client
|
ENV SERVE_CLIENT /home/node/app/packages/client
|
||||||
ENV SERVE_ADMIN /home/node/app/packages/admin
|
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
|
EXPOSE 3000
|
||||||
# profiler port, only used if profiler is explicity running
|
# profiler port, only used if profiler is explicity running
|
||||||
EXPOSE 9229
|
EXPOSE 9229
|
||||||
|
|
||||||
ENTRYPOINT [ "/bin/sh" ]
|
ENTRYPOINT [ "/bin/sh" ]
|
||||||
CMD [ "./docker-start.sh" ]
|
CMD [ "./docker-start.sh" ]
|
|
@ -6332,6 +6332,12 @@
|
||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/grecaptcha": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/grecaptcha/-/grecaptcha-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-fFxMtjAvXXMYTzDFK5NpcVB7WHnrHVLl00QzEGpuFxSAC789io6M+vjcn+g5FTEamIJtJr/IHkCDsqvJxeWDyw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/http-errors": {
|
"node_modules/@types/http-errors": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||||
|
@ -16104,6 +16110,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/vite-react": "^3.0.0",
|
"@tsconfig/vite-react": "^3.0.0",
|
||||||
|
"@types/grecaptcha": "^3.0.9",
|
||||||
"@types/lodash.throttle": "^4.1.9",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/vite-react": "^3.0.0",
|
"@tsconfig/vite-react": "^3.0.0",
|
||||||
|
"@types/grecaptcha": "^3.0.9",
|
||||||
"@types/lodash.throttle": "^4.1.9",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
|
|
|
@ -62,6 +62,7 @@ export const InfoSidebar = () => {
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
<b>Build {__COMMIT_HASH__}</b>
|
<b>Build {__COMMIT_HASH__}</b>
|
||||||
|
<div id="grecaptcha-badge"></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from "@sc07-canvas/lib/src/net";
|
} from "@sc07-canvas/lib/src/net";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { handleAlert, handleDismiss } from "./alerts";
|
import { handleAlert, handleDismiss } from "./alerts";
|
||||||
|
import { Recaptcha } from "./recaptcha";
|
||||||
|
|
||||||
export interface INetworkEvents {
|
export interface INetworkEvents {
|
||||||
connected: () => void;
|
connected: () => void;
|
||||||
|
@ -91,6 +92,14 @@ class Network extends EventEmitter<INetworkEvents> {
|
||||||
console.log("Reconnect failed");
|
console.log("Reconnect failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.socket.on("recaptcha", (site_key) => {
|
||||||
|
Recaptcha.load(site_key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("recaptcha_challenge", (ack) => {
|
||||||
|
Recaptcha.executeChallenge(ack);
|
||||||
|
});
|
||||||
|
|
||||||
this.socket.on("user", (user) => {
|
this.socket.on("user", (user) => {
|
||||||
this.emit("user", user);
|
this.emit("user", user);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
class Recaptcha_ {
|
||||||
|
load(site_key: string) {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.setAttribute(
|
||||||
|
"src",
|
||||||
|
`https://www.google.com/recaptcha/api.js?render=explicit`
|
||||||
|
);
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
script.onload = () => {
|
||||||
|
grecaptcha.ready(() => {
|
||||||
|
grecaptcha.render("grecaptcha-badge", {
|
||||||
|
sitekey: site_key,
|
||||||
|
badge: "inline",
|
||||||
|
size: "invisible",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Google Recaptcha Loaded!");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
executeChallenge(ack: (token: string) => void) {
|
||||||
|
console.log("[Recaptcha] Received challenge request...");
|
||||||
|
grecaptcha.execute().then((token) => {
|
||||||
|
console.log("[Recaptcha] Sending challenge token back");
|
||||||
|
ack(token as any);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Recaptcha = new Recaptcha_();
|
|
@ -23,6 +23,9 @@ export interface ServerToClientEvents {
|
||||||
alert: (alert: IAlert) => void;
|
alert: (alert: IAlert) => void;
|
||||||
alert_dismiss: (id: string) => void;
|
alert_dismiss: (id: string) => void;
|
||||||
|
|
||||||
|
recaptcha: (site_key: string) => void;
|
||||||
|
recaptcha_challenge: (ack: (token: string) => void) => void;
|
||||||
|
|
||||||
/* --- subscribe events --- */
|
/* --- subscribe events --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
InstanceNotFound,
|
InstanceNotFound,
|
||||||
} from "../models/Instance";
|
} from "../models/Instance";
|
||||||
import { AuditLog } from "../models/AuditLog";
|
import { AuditLog } from "../models/AuditLog";
|
||||||
|
import { LogMan } from "../lib/LogMan";
|
||||||
|
|
||||||
const app = Router();
|
const app = Router();
|
||||||
const Logger = getLogger("HTTP/ADMIN");
|
const Logger = getLogger("HTTP/ADMIN");
|
||||||
|
@ -50,6 +51,25 @@ app.get("/check", (req, res) => {
|
||||||
res.send({ success: true });
|
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) => {
|
app.get("/canvas/size", async (req, res) => {
|
||||||
const config = Canvas.getCanvasConfig();
|
const config = Canvas.getCanvasConfig();
|
||||||
|
|
||||||
|
@ -86,6 +106,11 @@ app.post("/canvas/size", async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await Canvas.setSize(width, height);
|
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 user = (await User.fromAuthSession(req.session.user!))!;
|
||||||
const auditLog = AuditLog.Factory(user.sub)
|
const auditLog = AuditLog.Factory(user.sub)
|
||||||
.doing("CANVAS_SIZE")
|
.doing("CANVAS_SIZE")
|
||||||
|
@ -111,6 +136,9 @@ app.get("/canvas/freeze", async (req, res) => {
|
||||||
app.post("/canvas/freeze", async (req, res) => {
|
app.post("/canvas/freeze", async (req, res) => {
|
||||||
await Canvas.setFrozen(true);
|
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 user = (await User.fromAuthSession(req.session.user!))!;
|
||||||
const auditLog = AuditLog.Factory(user.sub)
|
const auditLog = AuditLog.Factory(user.sub)
|
||||||
.doing("CANVAS_FREEZE")
|
.doing("CANVAS_FREEZE")
|
||||||
|
@ -129,6 +157,9 @@ app.post("/canvas/freeze", async (req, res) => {
|
||||||
app.delete("/canvas/freeze", async (req, res) => {
|
app.delete("/canvas/freeze", async (req, res) => {
|
||||||
await Canvas.setFrozen(false);
|
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 user = (await User.fromAuthSession(req.session.user!))!;
|
||||||
const auditLog = AuditLog.Factory(user.sub)
|
const auditLog = AuditLog.Factory(user.sub)
|
||||||
.doing("CANVAS_UNFREEZE")
|
.doing("CANVAS_UNFREEZE")
|
||||||
|
@ -272,6 +303,13 @@ app.put("/canvas/undo", async (req, res) => {
|
||||||
? paletteColors.find((p) => p.hex === coveredPixel.color)?.id || -1
|
? paletteColors.find((p) => p.hex === coveredPixel.color)?.id || -1
|
||||||
: -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;
|
break;
|
||||||
}
|
}
|
||||||
case "rejected":
|
case "rejected":
|
||||||
|
|
|
@ -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
|
// run startup tasks, all of these need to be completed to serve
|
||||||
Promise.all([
|
Promise.all([
|
||||||
Redis.getClient(),
|
Redis.getClient(),
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { SocketServer } from "./SocketServer";
|
||||||
import { getLogger } from "./Logger";
|
import { getLogger } from "./Logger";
|
||||||
import { Pixel } from "@prisma/client";
|
import { Pixel } from "@prisma/client";
|
||||||
import { CanvasWorker } from "../workers/worker";
|
import { CanvasWorker } from "../workers/worker";
|
||||||
|
import { LogMan } from "./LogMan";
|
||||||
|
|
||||||
const Logger = getLogger("CANVAS");
|
const Logger = getLogger("CANVAS");
|
||||||
|
|
||||||
|
@ -182,7 +183,7 @@ class Canvas {
|
||||||
x: pixel.x,
|
x: pixel.x,
|
||||||
y: pixel.y,
|
y: pixel.y,
|
||||||
createdAt: { lt: pixel.createdAt },
|
createdAt: { lt: pixel.createdAt },
|
||||||
deletedAt: null,
|
deletedAt: null, // undone pixels will have this set
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
take: 1,
|
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;
|
return coveringPixel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,6 +400,8 @@ class Canvas {
|
||||||
hex,
|
hex,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
LogMan.log("mod_fill", user.sub, { from: start, to: end, hex });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPixel(
|
async setPixel(
|
||||||
|
@ -432,6 +440,11 @@ class Canvas {
|
||||||
await this.updateCanvasRedisAtPos(x, y);
|
await this.updateCanvasRedisAtPos(x, y);
|
||||||
|
|
||||||
Logger.info(`${user.sub} placed pixel at (${x}, ${y})`);
|
Logger.info(`${user.sub} placed pixel at (${x}, ${y})`);
|
||||||
|
LogMan.log(isModAction ? "mod_override" : "pixel_place", user.sub, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
hex,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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<EventName extends keyof SystemEvents>(
|
||||||
|
event: EventName,
|
||||||
|
data: SystemEvents[EventName]
|
||||||
|
): void;
|
||||||
|
log<EventName extends keyof UserEvents>(
|
||||||
|
event: EventName,
|
||||||
|
user: string,
|
||||||
|
data: UserEvents[EventName]
|
||||||
|
): void;
|
||||||
|
log<EventName extends keyof UserEvents | keyof SystemEvents>(
|
||||||
|
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<keyof UserEvents, "mod_fill">] =
|
||||||
|
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_();
|
|
@ -1,6 +1,11 @@
|
||||||
import winston, { format } from "winston";
|
import winston, { format } from "winston";
|
||||||
|
import path from "node:path";
|
||||||
import { createEnum } from "./utils";
|
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) => {
|
const formatter = format.printf((options) => {
|
||||||
let maxModuleWidth = 0;
|
let maxModuleWidth = 0;
|
||||||
for (const module of Object.values(LoggerType)) {
|
for (const module of Object.values(LoggerType)) {
|
||||||
|
@ -26,6 +31,18 @@ const Winston = winston.createLogger({
|
||||||
transports: [new winston.transports.Console()],
|
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([
|
export const LoggerType = createEnum([
|
||||||
"MAIN",
|
"MAIN",
|
||||||
"SETTINGS",
|
"SETTINGS",
|
||||||
|
@ -38,6 +55,7 @@ export const LoggerType = createEnum([
|
||||||
"JOB_WORKER",
|
"JOB_WORKER",
|
||||||
"CANVAS_WORK",
|
"CANVAS_WORK",
|
||||||
"WORKER_ROOT",
|
"WORKER_ROOT",
|
||||||
|
"RECAPTCHA",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const getLogger = (module?: keyof typeof LoggerType) =>
|
export const getLogger = (module?: keyof typeof LoggerType) =>
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { Socket } from "socket.io";
|
||||||
|
import { User } from "../models/User";
|
||||||
|
import { getLogger } from "./Logger";
|
||||||
|
import {
|
||||||
|
ClientToServerEvents,
|
||||||
|
ServerToClientEvents,
|
||||||
|
} from "@sc07-canvas/lib/src/net";
|
||||||
|
|
||||||
|
const Logger = getLogger("RECAPTCHA");
|
||||||
|
|
||||||
|
class Recaptcha_ {
|
||||||
|
disabled = false;
|
||||||
|
chance: number | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.disabled =
|
||||||
|
!process.env.RECAPTCHA_SITE_KEY ||
|
||||||
|
!process.env.RECAPTCHA_SECRET_KEY ||
|
||||||
|
!process.env.RECAPTCHA_PIXEL_CHANCE;
|
||||||
|
|
||||||
|
if (!process.env.RECAPTCHA_PIXEL_CHANCE) {
|
||||||
|
Logger.warn("No RECAPTCHA_PIXEL_CHANCE set, captchas will not be sent!");
|
||||||
|
} else {
|
||||||
|
this.chance = parseFloat(process.env.RECAPTCHA_PIXEL_CHANCE);
|
||||||
|
|
||||||
|
if (this.chance > 1 || this.chance < 0) {
|
||||||
|
this.chance = null;
|
||||||
|
this.disabled = true;
|
||||||
|
Logger.warn("RECAPTCHA_PIXEL_CHANCE is not within (0<x<1)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeChallenge(
|
||||||
|
socket: Socket<ClientToServerEvents, ServerToClientEvents>
|
||||||
|
): boolean {
|
||||||
|
if (this.disabled || !this.chance) return false;
|
||||||
|
|
||||||
|
if (Math.random() > this.chance) {
|
||||||
|
socket.emitWithAck("recaptcha_challenge").then((token) => {
|
||||||
|
this.verifyToken(token).then(async (data) => {
|
||||||
|
if (!data.success) {
|
||||||
|
this.notifyStaffOfError(data).then(() => {});
|
||||||
|
} else {
|
||||||
|
if (data.score < 0.5 || true) {
|
||||||
|
try {
|
||||||
|
const user = (await User.fromAuthSession(
|
||||||
|
socket.request.session.user!
|
||||||
|
))!;
|
||||||
|
this.notifyStaff(user, data.score).then(() => {});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyToken(
|
||||||
|
token: string
|
||||||
|
): Promise<
|
||||||
|
| { success: true; challenge_ts: string; hostname: string; score: number }
|
||||||
|
| { success: false; "error-codes": string[] }
|
||||||
|
> {
|
||||||
|
return await fetch(
|
||||||
|
`https://www.google.com/recaptcha/api/siteverify?secret=${process.env.RECAPTCHA_SECRET_KEY!}&response=${token}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
}
|
||||||
|
).then((a) => a.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async notifyStaff(user: User, score: number) {
|
||||||
|
return await fetch(process.env.DISCORD_WEBHOOK!, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: `User ${user.sub} got a low score ${score}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async notifyStaffOfError(obj: any) {
|
||||||
|
return await fetch(process.env.DISCORD_WEBHOOK!, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content:
|
||||||
|
"Error while verifying captcha\n```\n" +
|
||||||
|
JSON.stringify(obj, null, 2) +
|
||||||
|
"\n```",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Recaptcha = new Recaptcha_();
|
|
@ -15,6 +15,7 @@ import { prisma } from "./prisma";
|
||||||
import { getLogger } from "./Logger";
|
import { getLogger } from "./Logger";
|
||||||
import { Redis } from "./redis";
|
import { Redis } from "./redis";
|
||||||
import { User } from "../models/User";
|
import { User } from "../models/User";
|
||||||
|
import { Recaptcha } from "./Recaptcha";
|
||||||
|
|
||||||
const Logger = getLogger("SOCKET");
|
const Logger = getLogger("SOCKET");
|
||||||
|
|
||||||
|
@ -194,6 +195,9 @@ export class SocketServer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.RECAPTCHA_SITE_KEY)
|
||||||
|
socket.emit("recaptcha", process.env.RECAPTCHA_SITE_KEY);
|
||||||
|
|
||||||
socket.emit("config", getClientConfig());
|
socket.emit("config", getClientConfig());
|
||||||
{
|
{
|
||||||
let _clientNotifiedAboutCache = false;
|
let _clientNotifiedAboutCache = false;
|
||||||
|
@ -298,6 +302,8 @@ export class SocketServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Recaptcha.maybeChallenge(socket);
|
||||||
|
|
||||||
await user.modifyStack(-1);
|
await user.modifyStack(-1);
|
||||||
await Canvas.setPixel(
|
await Canvas.setPixel(
|
||||||
user,
|
user,
|
||||||
|
|
|
@ -60,6 +60,14 @@ declare global {
|
||||||
MATRIX_HOMESERVER: string;
|
MATRIX_HOMESERVER: string;
|
||||||
ELEMENT_HOST: string;
|
ELEMENT_HOST: string;
|
||||||
MATRIX_GENERAL_ALIAS: string;
|
MATRIX_GENERAL_ALIAS: string;
|
||||||
|
|
||||||
|
PIXEL_LOG_PATH?: string;
|
||||||
|
|
||||||
|
RECAPTCHA_SITE_KEY?: string;
|
||||||
|
RECAPTCHA_SECRET_KEY?: string;
|
||||||
|
RECAPTCHA_PIXEL_CHANCE?: string;
|
||||||
|
|
||||||
|
DISCORD_WEBHOOK?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue