From 9a7ca41eb93dcb2cb9fac036ea81d261a4eb5c29 Mon Sep 17 00:00:00 2001
From: HF
Date: Sun, 29 Nov 2020 14:22:14 +0100
Subject: [PATCH] create Moderator role
---
src/components/Admintools.jsx | 230 ++++++++++++++---
src/components/UserAreaModal.jsx | 2 +-
src/core/ChatProvider.js | 4 +-
src/core/adminfunctions.js | 406 ++++++++++++++++++++++++++++++
src/core/draw.js | 19 +-
src/core/logger.js | 14 ++
src/data/models/RegUser.js | 16 ++
src/data/models/User.js | 36 ++-
src/reducers/user.js | 2 +-
src/routes/admintools.js | 420 +++++--------------------------
10 files changed, 724 insertions(+), 425 deletions(-)
create mode 100644 src/core/adminfunctions.js
diff --git a/src/components/Admintools.jsx b/src/components/Admintools.jsx
index 4904723..4b3d667 100644
--- a/src/components/Admintools.jsx
+++ b/src/components/Admintools.jsx
@@ -4,7 +4,7 @@
* @flow
*/
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import type { State } from '../reducers';
@@ -96,10 +96,60 @@ async function submitIPAction(
callback(await resp.text());
}
+async function getModList(
+ callback,
+) {
+ const data = new FormData();
+ data.append('modlist', true);
+ const resp = await fetch('./admintools', {
+ credentials: 'include',
+ method: 'POST',
+ body: data,
+ });
+ if (resp.ok) {
+ callback(await resp.json());
+ } else {
+ callback([]);
+ }
+}
+
+async function submitRemMod(
+ userId,
+ callback,
+) {
+ const data = new FormData();
+ data.append('remmod', userId);
+ const resp = await fetch('./admintools', {
+ credentials: 'include',
+ method: 'POST',
+ body: data,
+ });
+ callback(resp.ok, await resp.text());
+}
+
+async function submitMakeMod(
+ userName,
+ callback,
+) {
+ const data = new FormData();
+ data.append('makemod', userName);
+ const resp = await fetch('./admintools', {
+ credentials: 'include',
+ method: 'POST',
+ body: data,
+ });
+ if (resp.ok) {
+ callback(await resp.json());
+ } else {
+ callback(await resp.text());
+ }
+}
+
function Admintools({
canvasId,
canvases,
+ userlvl,
}) {
const curDate = new Date();
let day = curDate.getDate();
@@ -118,7 +168,9 @@ function Admintools({
const [brcoords, selectBRCoords] = useState(keptState.brcoords);
const [tlrcoords, selectTLRCoords] = useState(keptState.tlrcoords);
const [brrcoords, selectBRRCoords] = useState(keptState.brrcoords);
+ const [modName, selectModName] = useState(null);
const [resp, setResp] = useState(null);
+ const [modlist, setModList] = useState([]);
const [submitting, setSubmitting] = useState(false);
let descAction;
@@ -136,6 +188,12 @@ function Admintools({
// nothing
}
+ useEffect(() => {
+ if (userlvl) {
+ getModList((mods) => setModList(mods));
+ }
+ }, []);
+
return (
{resp && (
@@ -403,52 +461,144 @@ function Admintools({
{(submitting) ? '...' : 'Submit'}
-
-
- IP Actions
- Do stuff with IPs (one IP per line)
-
-
-
-
+ {['ban', 'unban', 'whitelist', 'unwhitelist'].map((opt) => (
+
+ ))}
+
+
+
+
+
+
+
+ Manage Moderators
+
+ Remove Moderator
+
+ {(modlist.length) ? (
+
+ {modlist.map((mod) => (
+ {
+ if (submitting) {
+ return;
+ }
+ setSubmitting(true);
+ submitRemMod(mod[0], (success, ret) => {
+ if (success) {
+ setModList(
+ modlist.filter((modl) => (modl[0] !== mod[0])),
+ );
+ }
+ setSubmitting(false);
+ setResp(ret);
+ });
+ }}
+ >
+ {`⦸ ${mod[0]} ${mod[1]}`}
+
+ ))}
+
+ )
+ : (
+ There are no mods
+ )}
+
+
+
+ Assign new Mod
+
+
+ Enter UserName of new Mod:
+ {
+ const co = evt.target.value.trim();
+ selectModName(co);
+ }}
+ />
+
+
+
+
+
+
+ )}
);
}
function mapStateToProps(state: State) {
const { canvasId, canvases } = state.canvas;
- return { canvasId, canvases };
+ const { userlvl } = state.user;
+ return { canvasId, canvases, userlvl };
}
export default connect(mapStateToProps)(Admintools);
diff --git a/src/components/UserAreaModal.jsx b/src/components/UserAreaModal.jsx
index 2582005..659842d 100644
--- a/src/components/UserAreaModal.jsx
+++ b/src/components/UserAreaModal.jsx
@@ -116,7 +116,7 @@ const UserAreaModal = ({
{userlvl && (
-
+
Loading...
}>
diff --git a/src/core/ChatProvider.js b/src/core/ChatProvider.js
index d029d82..5df1d78 100644
--- a/src/core/ChatProvider.js
+++ b/src/core/ChatProvider.js
@@ -223,7 +223,7 @@ export class ChatProvider {
const { id } = user;
const name = user.getName();
- if (!user.isAdmin() && await cheapDetector(user.ip)) {
+ if (!user.userlvl && await cheapDetector(user.ip)) {
logger.info(
`${name} / ${user.ip} tried to send chat message with proxy`,
);
@@ -235,7 +235,7 @@ export class ChatProvider {
return 'Couldn\'t send your message, pls log out and back in again.';
}
- if (user.isAdmin() && message.charAt(0) === '/') {
+ if (message.charAt(0) === '/' && user.userlvl) {
return this.adminCommands(message, channelId);
}
diff --git a/src/core/adminfunctions.js b/src/core/adminfunctions.js
new file mode 100644
index 0000000..886ed26
--- /dev/null
+++ b/src/core/adminfunctions.js
@@ -0,0 +1,406 @@
+/*
+ * functions for admintools
+ *
+ * @flow
+ */
+
+/* eslint-disable no-await-in-loop */
+
+import sharp from 'sharp';
+import Sequelize from 'sequelize';
+import redis from '../data/redis';
+
+import { admintoolsLogger } from './logger';
+import { getIPv6Subnet } from '../utils/ip';
+import { Blacklist, Whitelist, RegUser } from '../data/models';
+// eslint-disable-next-line import/no-unresolved
+import canvases from './canvases.json';
+import {
+ imageABGR2Canvas,
+ protectCanvasArea,
+} from './Image';
+import rollbackCanvasArea from './rollback';
+
+/*
+ * Execute IP based actions (banning, whitelist, etc.)
+ * @param action what to do with the ip
+ * @param ip already sanizized ip
+ * @return true if successful
+ */
+export async function executeIPAction(action: string, ips: string): string {
+ const ipArray = ips.split('\n');
+ let out = '';
+ const splitRegExp = /\s+/;
+ for (let i = 0; i < ipArray.length; i += 1) {
+ let ip = ipArray[i].trim();
+ const ipLine = ip.split(splitRegExp);
+ if (ipLine.length === 7) {
+ // logger output
+ // eslint-disable-next-line prefer-destructuring
+ ip = ipLine[2];
+ }
+ if (!ip || ip.length < 8 || ip.indexOf(' ') !== -1) {
+ out += `Couln't parse ${action} ${ip}\n`;
+ continue;
+ }
+ const ipKey = getIPv6Subnet(ip);
+ const key = `isprox:${ipKey}`;
+
+ admintoolsLogger.info(`ADMINTOOLS: ${action} ${ip}`);
+ switch (action) {
+ case 'ban':
+ await Blacklist.findOrCreate({
+ where: { ip: ipKey },
+ });
+ await redis.setAsync(key, 'y', 'EX', 24 * 3600);
+ break;
+ case 'unban':
+ await Blacklist.destroy({
+ where: { ip: ipKey },
+ });
+ await redis.del(key);
+ break;
+ case 'whitelist':
+ await Whitelist.findOrCreate({
+ where: { ip: ipKey },
+ });
+ await redis.setAsync(key, 'n', 'EX', 24 * 3600);
+ break;
+ case 'unwhitelist':
+ await Whitelist.destroy({
+ where: { ip: ipKey },
+ });
+ await redis.del(key);
+ break;
+ default:
+ out += `Failed to ${action} ${ip}\n`;
+ }
+ out += `Succseefully did ${action} ${ip}\n`;
+ }
+ return out;
+}
+
+/*
+ * Execute Image based actions (upload, protect, etc.)
+ * @param action what to do with the image
+ * @param file imagefile
+ * @param coords coord sin X_Y format
+ * @param canvasid numerical canvas id as string
+ * @return [ret, msg] http status code and message
+ */
+export async function executeImageAction(
+ action: string,
+ file: Object,
+ coords: string,
+ canvasid: string,
+) {
+ if (!coords) {
+ return [403, 'Coordinates not defined'];
+ }
+ if (!canvasid) {
+ return [403, 'canvasid not defined'];
+ }
+
+ const splitCoords = coords.trim().split('_');
+ if (splitCoords.length !== 2) {
+ return [403, 'Invalid Coordinate Format'];
+ }
+ const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
+
+ const canvas = canvases[canvasid];
+
+ let error = null;
+ if (Number.isNaN(x)) {
+ error = 'x is not a valid number';
+ } else if (Number.isNaN(y)) {
+ error = 'y is not a valid number';
+ } else if (!action) {
+ error = 'No imageaction given';
+ } else if (!canvas) {
+ error = 'Invalid canvas selected';
+ } else if (canvas.v) {
+ error = 'Can not upload Image to 3D canvas';
+ }
+ if (error !== null) {
+ return [403, error];
+ }
+
+ const canvasMaxXY = canvas.size / 2;
+ const canvasMinXY = -canvasMaxXY;
+ if (x < canvasMinXY || y < canvasMinXY
+ || x >= canvasMaxXY || y >= canvasMaxXY) {
+ return [403, 'Coordinates are outside of canvas'];
+ }
+
+ const protect = (action === 'protect');
+ const wipe = (action === 'wipe');
+
+ try {
+ const { data, info } = await sharp(file.buffer)
+ .ensureAlpha()
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+
+ const pxlCount = await imageABGR2Canvas(
+ canvasid,
+ x, y,
+ data,
+ info.width, info.height,
+ wipe, protect,
+ );
+
+ // eslint-disable-next-line max-len
+ admintoolsLogger.info(`ADMINTOOLS: Loaded image wth ${pxlCount} pixels to ${x}/${y}`);
+ return [
+ 200,
+ `Successfully loaded image wth ${pxlCount} pixels to ${x}/${y}`,
+ ];
+ } catch {
+ return [400, 'Can not read image file'];
+ }
+}
+
+/*
+ * Execute actions for protecting areas
+ * @param action what to do
+ * @param ulcoor coords of upper-left corner in X_Y format
+ * @param brcoor coords of bottom-right corner in X_Y format
+ * @param canvasid numerical canvas id as string
+ * @return [ret, msg] http status code and message
+ */
+export async function executeProtAction(
+ action: string,
+ ulcoor: string,
+ brcoor: string,
+ canvasid: number,
+) {
+ if (!ulcoor || !brcoor) {
+ return [403, 'Not all coordinates defined'];
+ }
+ if (!canvasid) {
+ return [403, 'canvasid not defined'];
+ }
+
+ let splitCoords = ulcoor.trim().split('_');
+ if (splitCoords.length !== 2) {
+ return [403, 'Invalid Coordinate Format for top-left corner'];
+ }
+ const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
+ splitCoords = brcoor.trim().split('_');
+ if (splitCoords.length !== 2) {
+ return [403, 'Invalid Coordinate Format for bottom-right corner'];
+ }
+ const [u, v] = splitCoords.map((z) => Math.floor(Number(z)));
+
+ const canvas = canvases[canvasid];
+
+ let error = null;
+ if (Number.isNaN(x)) {
+ error = 'x of top-left corner is not a valid number';
+ } else if (Number.isNaN(y)) {
+ error = 'y of top-left corner is not a valid number';
+ } else if (Number.isNaN(u)) {
+ error = 'x of bottom-right corner is not a valid number';
+ } else if (Number.isNaN(v)) {
+ error = 'y of bottom-right corner is not a valid number';
+ } else if (u < x || v < y) {
+ error = 'Corner coordinates are alligned wrong';
+ } else if (!action) {
+ error = 'No imageaction given';
+ } else if (!canvas) {
+ error = 'Invalid canvas selected';
+ } else if (action !== 'protect' && action !== 'unprotect') {
+ error = 'Invalid action (must be protect or unprotect)';
+ }
+ if (error !== null) {
+ return [403, error];
+ }
+
+ const canvasMaxXY = canvas.size / 2;
+ const canvasMinXY = -canvasMaxXY;
+ if (x < canvasMinXY || y < canvasMinXY
+ || x >= canvasMaxXY || y >= canvasMaxXY) {
+ return [403, 'Coordinates of top-left corner are outside of canvas'];
+ }
+ if (u < canvasMinXY || v < canvasMinXY
+ || u >= canvasMaxXY || v >= canvasMaxXY) {
+ return [403, 'Coordinates of bottom-right corner are outside of canvas'];
+ }
+
+ const width = u - x + 1;
+ const height = v - y + 1;
+ const protect = action === 'protect';
+ const pxlCount = await protectCanvasArea(
+ canvasid,
+ x,
+ y,
+ width,
+ height,
+ protect,
+ );
+ admintoolsLogger.info(
+ // eslint-disable-next-line max-len
+ `ADMINTOOLS: Set protect to ${protect} for ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
+ );
+ return [
+ 200,
+ (protect)
+ // eslint-disable-next-line max-len
+ ? `Successfully protected ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`
+ // eslint-disable-next-line max-len
+ : `Soccessfully unprotected ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
+ ];
+}
+
+/*
+ * Execute rollback
+ * @param date in format YYYYMMdd
+ * @param ulcoor coords of upper-left corner in X_Y format
+ * @param brcoor coords of bottom-right corner in X_Y format
+ * @param canvasid numerical canvas id as string
+ * @return [ret, msg] http status code and message
+ */
+export async function executeRollback(
+ date: string,
+ ulcoor: string,
+ brcoor: string,
+ canvasid: number,
+) {
+ if (!ulcoor || !brcoor) {
+ return [403, 'Not all coordinates defined'];
+ }
+ if (!canvasid) {
+ return [403, 'canvasid not defined'];
+ }
+
+ let splitCoords = ulcoor.trim().split('_');
+ if (splitCoords.length !== 2) {
+ return [403, 'Invalid Coordinate Format for top-left corner'];
+ }
+ const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
+ splitCoords = brcoor.trim().split('_');
+ if (splitCoords.length !== 2) {
+ return [403, 'Invalid Coordinate Format for bottom-right corner'];
+ }
+ const [u, v] = splitCoords.map((z) => Math.floor(Number(z)));
+
+ const canvas = canvases[canvasid];
+
+ let error = null;
+ if (Number.isNaN(x)) {
+ error = 'x of top-left corner is not a valid number';
+ } else if (Number.isNaN(y)) {
+ error = 'y of top-left corner is not a valid number';
+ } else if (Number.isNaN(u)) {
+ error = 'x of bottom-right corner is not a valid number';
+ } else if (Number.isNaN(v)) {
+ error = 'y of bottom-right corner is not a valid number';
+ } else if (u < x || v < y) {
+ error = 'Corner coordinates are alligned wrong';
+ } else if (!date) {
+ error = 'No date given';
+ } else if (Number.isNaN(Number(date))) {
+ error = 'Invalid date';
+ } else if (!canvas) {
+ error = 'Invalid canvas selected';
+ }
+ if (error !== null) {
+ return [403, error];
+ }
+
+ const canvasMaxXY = canvas.size / 2;
+ const canvasMinXY = -canvasMaxXY;
+ if (x < canvasMinXY || y < canvasMinXY
+ || x >= canvasMaxXY || y >= canvasMaxXY) {
+ return [403, 'Coordinates of top-left corner are outside of canvas'];
+ }
+ if (u < canvasMinXY || v < canvasMinXY
+ || u >= canvasMaxXY || v >= canvasMaxXY) {
+ return [403, 'Coordinates of bottom-right corner are outside of canvas'];
+ }
+
+ const width = u - x + 1;
+ const height = v - y + 1;
+ if (width * height > 1000000) {
+ return [403, 'Can not rollback more than 1m pixels at onec'];
+ }
+
+ const pxlCount = await rollbackCanvasArea(
+ canvasid,
+ x,
+ y,
+ width,
+ height,
+ date,
+ );
+ admintoolsLogger.info(
+ // eslint-disable-next-line max-len
+ `ADMINTOOLS: Rollback to ${date} for ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
+ );
+ return [
+ 200,
+ // eslint-disable-next-line max-len
+ `Successfully rolled back ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
+ ];
+}
+
+/*
+ * Get list of mods
+ * @return [[id1, name2], [id2, name2], ...] list
+ */
+export async function getModList() {
+ const mods = await RegUser.findAll({
+ where: Sequelize.where(Sequelize.literal('roles & 1'), '!=', 0),
+ attributes: ['id', 'name'],
+ raw: true,
+ });
+ return mods.map((mod) => [mod.id, mod.name]);
+}
+
+export async function removeMod(userId) {
+ if (Number.isNaN(userId)) {
+ throw new Error('Invalid userId');
+ }
+ let user = null;
+ try {
+ user = await RegUser.findByPk(userId);
+ } catch {
+ throw new Error('Database error on remove mod');
+ }
+ if (!user) {
+ throw new Error('User not found');
+ }
+ try {
+ await user.update({
+ isMod: false,
+ });
+ return `Moderation rights removed from user ${userId}`;
+ } catch {
+ throw new Error('Couldn\'t remove Mod from user');
+ }
+}
+
+export async function makeMod(name) {
+ let user = null;
+ try {
+ user = await RegUser.findOne({
+ where: {
+ name,
+ },
+ });
+ } catch {
+ throw new Error(`Invalid user ${name}`);
+ }
+ if (!user) {
+ throw new Error(`User ${name} not found`);
+ }
+ try {
+ await user.update({
+ isMod: true,
+ });
+ return [user.id, user.name];
+ } catch {
+ throw new Error('Couldn\'t remove Mod from user');
+ }
+}
+
diff --git a/src/core/draw.js b/src/core/draw.js
index 3b0fe01..e1734e8 100644
--- a/src/core/draw.js
+++ b/src/core/draw.js
@@ -88,20 +88,22 @@ export async function drawByOffset(
}
}
+ const isAdmin = (user.userlvl === 1);
const setColor = await RedisCanvas.getPixelByOffset(canvasId, i, j, offset);
+
if (setColor & 0x80
/* 3D Canvas Minecraft Avatars */
// && x >= 96 && x <= 128 && z >= 35 && z <= 100
// 96 - 128 on x
// 32 - 128 on z
- || (canvas.v && i === 19 && j >= 17 && j < 20 && !user.isAdmin())
+ || (canvas.v && i === 19 && j >= 17 && j < 20 && !isAdmin)
) {
// protected pixel
throw new Error(8);
}
coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd;
- if (user.isAdmin()) {
+ if (isAdmin) {
coolDown = 0.0;
} else if (rpgEvent.success) {
if (rpgEvent.success === 1) {
@@ -262,10 +264,11 @@ export async function drawByCoords(
}
}
+ const isAdmin = (user.userlvl === 1);
const setColor = await RedisCanvas.getPixel(canvasId, x, y, z);
let coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd;
- if (user.isAdmin()) {
+ if (isAdmin) {
coolDown = 0.0;
} else if (rpgEvent.success) {
if (rpgEvent.success === 1) {
@@ -293,7 +296,7 @@ export async function drawByCoords(
if (setColor & 0x80
|| (canvas.v
&& x >= 96 && x <= 128 && z >= 35 && z <= 100
- && !user.isAdmin())
+ && !isAdmin)
) {
logger.info(`${user.ip} tried to set on protected pixel (${x}, ${y})`);
return {
@@ -338,10 +341,6 @@ export function drawSafeByCoords(
y: number,
z: number = null,
): Promise
{
- if (user.isAdmin()) {
- return drawByCoords(user, canvasId, color, x, y, z);
- }
-
// can just check for one unique occurence,
// we use ip, because id for logged out users is
// always null
@@ -379,10 +378,6 @@ export function drawSafeByOffset(
j: number,
offset: number,
): Promise {
- if (user.isAdmin()) {
- return drawByOffset(user, canvasId, color, i, j, offset);
- }
-
// can just check for one unique occurence,
// we use ip, because id for logged out users is
// always null
diff --git a/src/core/logger.js b/src/core/logger.js
index 51704b3..823cd44 100644
--- a/src/core/logger.js
+++ b/src/core/logger.js
@@ -48,5 +48,19 @@ export const proxyLogger = createLogger({
],
});
+export const admintoolsLogger = createLogger({
+ format: format.printf(({ message }) => message),
+ transports: [
+ new DailyRotateFile({
+ level: 'info',
+ filename: './log/admintools-%DATE%.log',
+ maxSize: '20m',
+ maxFiles: '14d',
+ colorize: false,
+ }),
+ ],
+});
+
+
export default logger;
diff --git a/src/data/models/RegUser.js b/src/data/models/RegUser.js
index 233f492..977b5da 100644
--- a/src/data/models/RegUser.js
+++ b/src/data/models/RegUser.js
@@ -29,6 +29,13 @@ const RegUser = Model.define('User', {
allowNull: false,
},
+ // currently just moderator
+ roles: {
+ type: DataType.TINYINT,
+ allowNull: false,
+ defaultValue: 0,
+ },
+
// null if external oauth authentification
password: {
type: DataType.CHAR(60),
@@ -125,6 +132,10 @@ const RegUser = Model.define('User', {
blockDm(): boolean {
return this.blocks & 0x01;
},
+
+ isMod(): boolean {
+ return this.roles & 0x01;
+ },
},
setterMethods: {
@@ -143,6 +154,11 @@ const RegUser = Model.define('User', {
this.setDataValue('blocks', val);
},
+ isMod(num: boolean) {
+ const val = (num) ? (this.roles | 0x01) : (this.roles & ~0x01);
+ this.setDataValue('roles', val);
+ },
+
password(value: string) {
if (value) this.setDataValue('password', generateHash(value));
},
diff --git a/src/data/models/User.js b/src/data/models/User.js
index a0e398e..40f89b7 100644
--- a/src/data/models/User.js
+++ b/src/data/models/User.js
@@ -22,7 +22,14 @@ class User {
ip: string;
wait: ?number;
regUser: Object;
- channels: Array;
+ channels: Object;
+ blocked: Array;
+ /*
+ * 0: nothing
+ * 1: Admin
+ * 2: Mod
+ */
+ userlvl: number;
constructor(id: string = null, ip: string = '127.0.0.1') {
// id should stay null if unregistered
@@ -30,6 +37,7 @@ class User {
this.ip = ip;
this.channels = {};
this.blocked = [];
+ this.userlvl = 0;
this.ipSub = getIPv6Subnet(ip);
this.wait = null;
// following gets populated by passport
@@ -54,6 +62,14 @@ class User {
setRegUser(reguser) {
this.regUser = reguser;
this.id = reguser.id;
+
+ if (this.regUser.isMod) {
+ this.userlvl = 2;
+ }
+ if (ADMIN_IDS.includes(this.id)) {
+ this.userlvl = 1;
+ }
+
if (reguser.channel) {
for (let i = 0; i < reguser.channel.length; i += 1) {
const {
@@ -139,7 +155,7 @@ class User {
async incrementPixelcount(): Promise {
const { id } = this;
if (!id) return false;
- if (this.isAdmin()) return false;
+ if (this.userlvl === 1) return false;
try {
await RegUser.update({
totalPixels: Sequelize.literal('totalPixels + 1'),
@@ -156,7 +172,7 @@ class User {
async getTotalPixels(): Promise {
const { id } = this;
if (!id) return 0;
- if (this.isAdmin()) return 100000;
+ if (this.userlvl === 1) return 100000;
if (this.regUser) {
return this.regUser.totalPixels;
}
@@ -196,10 +212,6 @@ class User {
return true;
}
- isAdmin(): boolean {
- return ADMIN_IDS.includes(this.id);
- }
-
getUserData(): Object {
if (this.regUser == null) {
return {
@@ -218,7 +230,9 @@ class User {
blocked: this.blocked,
};
}
- const { regUser } = this;
+ const {
+ regUser, userlvl, channels, blocked,
+ } = this;
return {
name: regUser.name,
mailVerified: regUser.mailVerified,
@@ -230,9 +244,9 @@ class User {
ranking: regUser.ranking,
dailyRanking: regUser.dailyRanking,
mailreg: !!(regUser.password),
- userlvl: this.isAdmin() ? 1 : 0,
- channels: this.channels,
- blocked: this.blocked,
+ userlvl,
+ channels,
+ blocked,
};
}
}
diff --git a/src/reducers/user.js b/src/reducers/user.js
index 7993fb5..76f09e2 100644
--- a/src/reducers/user.js
+++ b/src/reducers/user.js
@@ -33,7 +33,7 @@ export type UserState = {
isOnMobile: boolean,
// small notifications for received cooldown
notification: string,
- // 1: Admin, 0: ordinary user
+ // 1: Admin, 2: Mod, 0: ordinary user
userlvl: number,
// regExp for detecting ping
nameRegExp: RegExp,
diff --git a/src/routes/admintools.js b/src/routes/admintools.js
index da688cd..32fb011 100644
--- a/src/routes/admintools.js
+++ b/src/routes/admintools.js
@@ -6,30 +6,27 @@
*
*/
-/* eslint-disable no-await-in-loop */
-
import express from 'express';
import expressLimiter from 'express-limiter';
import type { Request, Response } from 'express';
import bodyParser from 'body-parser';
-import sharp from 'sharp';
import multer from 'multer';
-import { getIPFromRequest, getIPv6Subnet } from '../utils/ip';
+import { getIPFromRequest } from '../utils/ip';
import redis from '../data/redis';
import session from '../core/session';
import passport from '../core/passport';
-import logger from '../core/logger';
-import { Blacklist, Whitelist } from '../data/models';
-
+import { admintoolsLogger } from '../core/logger';
import { MINUTE } from '../core/constants';
-// eslint-disable-next-line import/no-unresolved
-import canvases from './canvases.json';
import {
- imageABGR2Canvas,
- protectCanvasArea,
-} from '../core/Image';
-import rollbackCanvasArea from '../core/rollback';
+ executeIPAction,
+ executeImageAction,
+ executeProtAction,
+ executeRollback,
+ getModList,
+ removeMod,
+ makeMod,
+} from '../core/adminfunctions';
const router = express.Router();
@@ -50,6 +47,7 @@ const upload = multer({
/*
* rate limiting to prevent bruteforce attacks
+ * TODO: do that with nginx
*/
router.use('/',
limiter({
@@ -61,7 +59,7 @@ router.use('/',
/*
- * make sure User is logged in and admin
+ * make sure User is logged in and mod or admin
*/
router.use(session);
router.use(passport.initialize());
@@ -69,350 +67,31 @@ router.use(passport.session());
router.use(async (req, res, next) => {
const ip = getIPFromRequest(req);
if (!req.user) {
- logger.info(`ADMINTOOLS: ${ip} tried to access admintools without login`);
+ admintoolsLogger.info(`ADMINTOOLS: ${ip} tried to access admintools without login`);
res.status(403).send('You are not logged in');
return;
}
- if (!req.user.isAdmin()) {
- logger.info(
+ /*
+ * 1 = Admin
+ * 2 = Mod
+ */
+ if (!req.user.userlvl) {
+ admintoolsLogger.info(
`ADMINTOOLS: ${ip} / ${req.user.id} tried to access admintools`,
);
res.status(403).send('You are not allowed to access this page');
return;
}
- logger.info(
+ admintoolsLogger.info(
`ADMINTOOLS: ${req.user.id} / ${req.user.regUser.name} is using admintools`,
);
+
next();
});
/*
- * Execute IP based actions (banning, whitelist, etc.)
- * @param action what to do with the ip
- * @param ip already sanizized ip
- * @return true if successful
- */
-async function executeIPAction(action: string, ips: string): boolean {
- const ipArray = ips.split('\n');
- let out = '';
- const splitRegExp = /\s+/;
- for (let i = 0; i < ipArray.length; i += 1) {
- let ip = ipArray[i].trim();
- const ipLine = ip.split(splitRegExp);
- if (ipLine.length === 7) {
- // logger output
- // eslint-disable-next-line prefer-destructuring
- ip = ipLine[2];
- }
- if (!ip || ip.length < 8 || ip.indexOf(' ') !== -1) {
- out += `Couln't parse ${action} ${ip}\n`;
- continue;
- }
- const ipKey = getIPv6Subnet(ip);
- const key = `isprox:${ipKey}`;
-
- logger.info(`ADMINTOOLS: ${action} ${ip}`);
- switch (action) {
- case 'ban':
- await Blacklist.findOrCreate({
- where: { ip: ipKey },
- });
- await redis.setAsync(key, 'y', 'EX', 24 * 3600);
- break;
- case 'unban':
- await Blacklist.destroy({
- where: { ip: ipKey },
- });
- await redis.del(key);
- break;
- case 'whitelist':
- await Whitelist.findOrCreate({
- where: { ip: ipKey },
- });
- await redis.setAsync(key, 'n', 'EX', 24 * 3600);
- break;
- case 'unwhitelist':
- await Whitelist.destroy({
- where: { ip: ipKey },
- });
- await redis.del(key);
- break;
- default:
- out += `Failed to ${action} ${ip}\n`;
- }
- out += `Succseefully did ${action} ${ip}\n`;
- }
- return out;
-}
-
-/*
- * Execute Image based actions (upload, protect, etc.)
- * @param action what to do with the image
- * @param file imagefile
- * @param coords coord sin X_Y format
- * @param canvasid numerical canvas id as string
- * @return [ret, msg] http status code and message
- */
-async function executeImageAction(
- action: string,
- file: Object,
- coords: string,
- canvasid: string,
-) {
- if (!coords) {
- return [403, 'Coordinates not defined'];
- }
- if (!canvasid) {
- return [403, 'canvasid not defined'];
- }
-
- const splitCoords = coords.trim().split('_');
- if (splitCoords.length !== 2) {
- return [403, 'Invalid Coordinate Format'];
- }
- const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
-
- const canvas = canvases[canvasid];
-
- let error = null;
- if (Number.isNaN(x)) {
- error = 'x is not a valid number';
- } else if (Number.isNaN(y)) {
- error = 'y is not a valid number';
- } else if (!action) {
- error = 'No imageaction given';
- } else if (!canvas) {
- error = 'Invalid canvas selected';
- } else if (canvas.v) {
- error = 'Can not upload Image to 3D canvas';
- }
- if (error !== null) {
- return [403, error];
- }
-
- const canvasMaxXY = canvas.size / 2;
- const canvasMinXY = -canvasMaxXY;
- if (x < canvasMinXY || y < canvasMinXY
- || x >= canvasMaxXY || y >= canvasMaxXY) {
- return [403, 'Coordinates are outside of canvas'];
- }
-
- const protect = (action === 'protect');
- const wipe = (action === 'wipe');
-
- try {
- const { data, info } = await sharp(file.buffer)
- .ensureAlpha()
- .raw()
- .toBuffer({ resolveWithObject: true });
-
- const pxlCount = await imageABGR2Canvas(
- canvasid,
- x, y,
- data,
- info.width, info.height,
- wipe, protect,
- );
-
- // eslint-disable-next-line max-len
- logger.info(`ADMINTOOLS: Loaded image wth ${pxlCount} pixels to ${x}/${y}`);
- return [
- 200,
- `Successfully loaded image wth ${pxlCount} pixels to ${x}/${y}`,
- ];
- } catch {
- return [400, 'Can not read image file'];
- }
-}
-
-/*
- * Execute actions for protecting areas
- * @param action what to do
- * @param ulcoor coords of upper-left corner in X_Y format
- * @param brcoor coords of bottom-right corner in X_Y format
- * @param canvasid numerical canvas id as string
- * @return [ret, msg] http status code and message
- */
-async function executeProtAction(
- action: string,
- ulcoor: string,
- brcoor: string,
- canvasid: number,
-) {
- if (!ulcoor || !brcoor) {
- return [403, 'Not all coordinates defined'];
- }
- if (!canvasid) {
- return [403, 'canvasid not defined'];
- }
-
- let splitCoords = ulcoor.trim().split('_');
- if (splitCoords.length !== 2) {
- return [403, 'Invalid Coordinate Format for top-left corner'];
- }
- const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
- splitCoords = brcoor.trim().split('_');
- if (splitCoords.length !== 2) {
- return [403, 'Invalid Coordinate Format for bottom-right corner'];
- }
- const [u, v] = splitCoords.map((z) => Math.floor(Number(z)));
-
- const canvas = canvases[canvasid];
-
- let error = null;
- if (Number.isNaN(x)) {
- error = 'x of top-left corner is not a valid number';
- } else if (Number.isNaN(y)) {
- error = 'y of top-left corner is not a valid number';
- } else if (Number.isNaN(u)) {
- error = 'x of bottom-right corner is not a valid number';
- } else if (Number.isNaN(v)) {
- error = 'y of bottom-right corner is not a valid number';
- } else if (u < x || v < y) {
- error = 'Corner coordinates are alligned wrong';
- } else if (!action) {
- error = 'No imageaction given';
- } else if (!canvas) {
- error = 'Invalid canvas selected';
- } else if (action !== 'protect' && action !== 'unprotect') {
- error = 'Invalid action (must be protect or unprotect)';
- }
- if (error !== null) {
- return [403, error];
- }
-
- const canvasMaxXY = canvas.size / 2;
- const canvasMinXY = -canvasMaxXY;
- if (x < canvasMinXY || y < canvasMinXY
- || x >= canvasMaxXY || y >= canvasMaxXY) {
- return [403, 'Coordinates of top-left corner are outside of canvas'];
- }
- if (u < canvasMinXY || v < canvasMinXY
- || u >= canvasMaxXY || v >= canvasMaxXY) {
- return [403, 'Coordinates of bottom-right corner are outside of canvas'];
- }
-
- const width = u - x + 1;
- const height = v - y + 1;
- const protect = action === 'protect';
- const pxlCount = await protectCanvasArea(
- canvasid,
- x,
- y,
- width,
- height,
- protect,
- );
- logger.info(
- // eslint-disable-next-line max-len
- `ADMINTOOLS: Set protect to ${protect} for ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
- );
- return [
- 200,
- (protect)
- // eslint-disable-next-line max-len
- ? `Successfully protected ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`
- // eslint-disable-next-line max-len
- : `Soccessfully unprotected ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
- ];
-}
-
-/*
- * Execute rollback
- * @param date in format YYYYMMdd
- * @param ulcoor coords of upper-left corner in X_Y format
- * @param brcoor coords of bottom-right corner in X_Y format
- * @param canvasid numerical canvas id as string
- * @return [ret, msg] http status code and message
- */
-async function executeRollback(
- date: string,
- ulcoor: string,
- brcoor: string,
- canvasid: number,
-) {
- if (!ulcoor || !brcoor) {
- return [403, 'Not all coordinates defined'];
- }
- if (!canvasid) {
- return [403, 'canvasid not defined'];
- }
-
- let splitCoords = ulcoor.trim().split('_');
- if (splitCoords.length !== 2) {
- return [403, 'Invalid Coordinate Format for top-left corner'];
- }
- const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
- splitCoords = brcoor.trim().split('_');
- if (splitCoords.length !== 2) {
- return [403, 'Invalid Coordinate Format for bottom-right corner'];
- }
- const [u, v] = splitCoords.map((z) => Math.floor(Number(z)));
-
- const canvas = canvases[canvasid];
-
- let error = null;
- if (Number.isNaN(x)) {
- error = 'x of top-left corner is not a valid number';
- } else if (Number.isNaN(y)) {
- error = 'y of top-left corner is not a valid number';
- } else if (Number.isNaN(u)) {
- error = 'x of bottom-right corner is not a valid number';
- } else if (Number.isNaN(v)) {
- error = 'y of bottom-right corner is not a valid number';
- } else if (u < x || v < y) {
- error = 'Corner coordinates are alligned wrong';
- } else if (!date) {
- error = 'No date given';
- } else if (Number.isNaN(Number(date))) {
- error = 'Invalid date';
- } else if (!canvas) {
- error = 'Invalid canvas selected';
- }
- if (error !== null) {
- return [403, error];
- }
-
- const canvasMaxXY = canvas.size / 2;
- const canvasMinXY = -canvasMaxXY;
- if (x < canvasMinXY || y < canvasMinXY
- || x >= canvasMaxXY || y >= canvasMaxXY) {
- return [403, 'Coordinates of top-left corner are outside of canvas'];
- }
- if (u < canvasMinXY || v < canvasMinXY
- || u >= canvasMaxXY || v >= canvasMaxXY) {
- return [403, 'Coordinates of bottom-right corner are outside of canvas'];
- }
-
- const width = u - x + 1;
- const height = v - y + 1;
- if (width * height > 1000000) {
- return [403, 'Can not rollback more than 1m pixels at onec'];
- }
-
- const pxlCount = await rollbackCanvasArea(
- canvasid,
- x,
- y,
- width,
- height,
- date,
- );
- logger.info(
- // eslint-disable-next-line max-len
- `ADMINTOOLS: Rollback to ${date} for ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
- );
- return [
- 200,
- // eslint-disable-next-line max-len
- `Successfully rolled back ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
- ];
-}
-
-
-/*
- * Check for POST parameters,
+ * Post for mod + admin
*/
router.post('/', upload.single('image'), async (req, res, next) => {
try {
@@ -426,10 +105,6 @@ router.post('/', upload.single('image'), async (req, res, next) => {
);
res.status(ret).send(msg);
return;
- } if (req.body.ipaction) {
- const ret = await executeIPAction(req.body.ipaction, req.body.ip);
- res.status(200).send(ret);
- return;
} if (req.body.protaction) {
const {
protaction, ulcoor, brcoor, canvasid,
@@ -465,23 +140,52 @@ router.post('/', upload.single('image'), async (req, res, next) => {
/*
- * Check GET parameters for action to execute
+ * just admins past here, no Mods
*/
-router.get('/', async (req: Request, res: Response, next) => {
+router.use(async (req, res, next) => {
+ if (req.user.userlvl !== 1) {
+ res.status(403).send('Just admins can do that');
+ return;
+ }
+ next();
+});
+
+/*
+ * Post just for admin
+ */
+router.post('/', async (req, res, next) => {
try {
- const { ip, ipaction } = req.query;
- if (!ipaction) {
- next();
+ if (req.body.ipaction) {
+ const ret = await executeIPAction(req.body.ipaction, req.body.ip);
+ res.status(200).send(ret);
return;
}
- if (!ip) {
- res.status(400).json({ errors: 'invalid ip' });
+ if (req.body.modlist) {
+ const ret = await getModList();
+ res.status(200);
+ res.json(ret);
return;
}
-
- const ret = await executeIPAction(ipaction, ip);
-
- res.json({ ipaction: 'success', messages: ret.split('\n') });
+ if (req.body.remmod) {
+ try {
+ const ret = await removeMod(req.body.remmod);
+ res.status(200).send(ret);
+ } catch (e) {
+ res.status(400).send(e.message);
+ }
+ return;
+ }
+ if (req.body.makemod) {
+ try {
+ const ret = await makeMod(req.body.makemod);
+ res.status(200);
+ res.json(ret);
+ } catch (e) {
+ res.status(400).send(e.message);
+ }
+ return;
+ }
+ next();
} catch (error) {
next(error);
}
| |