create Moderator role

This commit is contained in:
HF 2020-11-29 14:22:14 +01:00
parent a9922e3041
commit 9a7ca41eb9
10 changed files with 724 additions and 425 deletions

View File

@ -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 (
<p style={{ textAlign: 'center', paddingLeft: '5%', paddingRight: '5%' }}>
{resp && (
@ -403,52 +461,144 @@ function Admintools({
{(submitting) ? '...' : 'Submit'}
</button>
<br />
<div className="modaldivider" />
<h3 className="modaltitle">IP Actions</h3>
<p className="modalcotext">Do stuff with IPs (one IP per line)</p>
<select
onChange={(e) => {
const sel = e.target;
selectIPAction(sel.options[sel.selectedIndex].value);
}}
>
{['ban', 'unban', 'whitelist', 'unwhitelist'].map((opt) => (
<option
value={opt}
selected={iPAction === opt}
{(userlvl === 1) && (
<div>
<br />
<div className="modaldivider" />
<h3 className="modaltitle">IP Actions</h3>
<p className="modalcotext">Do stuff with IPs (one IP per line)</p>
<select
onChange={(e) => {
const sel = e.target;
selectIPAction(sel.options[sel.selectedIndex].value);
}}
>
{opt}
</option>
))}
</select>
<br />
<textarea rows="10" cols="17" id="iparea" /><br />
<button
type="button"
onClick={() => {
if (submitting) {
return;
}
setSubmitting(true);
submitIPAction(
iPAction,
(ret) => {
setSubmitting(false);
setResp(ret);
},
);
}}
>
{(submitting) ? '...' : 'Submit'}
</button>
{['ban', 'unban', 'whitelist', 'unwhitelist'].map((opt) => (
<option
value={opt}
selected={iPAction === opt}
>
{opt}
</option>
))}
</select>
<br />
<textarea rows="10" cols="17" id="iparea" /><br />
<button
type="button"
onClick={() => {
if (submitting) {
return;
}
setSubmitting(true);
submitIPAction(
iPAction,
(ret) => {
setSubmitting(false);
setResp(ret);
},
);
}}
>
{(submitting) ? '...' : 'Submit'}
</button>
<br />
<div className="modaldivider" />
<h3 className="modaltitle">Manage Moderators</h3>
<p className="modalcotext">
Remove Moderator
</p>
{(modlist.length) ? (
<span
className="unblocklist"
>
{modlist.map((mod) => (
<div
role="button"
tabIndex={0}
onClick={() => {
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]}`}
</div>
))}
</span>
)
: (
<p className="modaltext">There are no mods</p>
)}
<br />
<p className="modalcotext">
Assign new Mod
</p>
<p className="modalcotext">
Enter UserName of new Mod:&nbsp;
<input
value={modName}
style={{
display: 'inline-block',
width: '100%',
maxWidth: '20em',
}}
type="text"
placeholder="User Name"
onChange={(evt) => {
const co = evt.target.value.trim();
selectModName(co);
}}
/>
</p>
<button
type="button"
onClick={() => {
if (submitting) {
return;
}
setSubmitting(true);
submitMakeMod(
modName,
(ret) => {
if (typeof ret === 'string') {
setResp(ret);
} else {
setResp(`Made ${ret[1]} mod successfully.`);
setModList([...modlist, ret]);
}
setSubmitting(false);
},
);
}}
>
{(submitting) ? '...' : 'Submit'}
</button>
<br />
<div className="modaldivider" />
<br />
</div>
)}
</p>
);
}
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);

View File

@ -116,7 +116,7 @@ const UserAreaModal = ({
</Suspense>
</div>
{userlvl && (
<div label="Admintools">
<div label={(userlvl === 1) ? 'Admintools' : 'Modtools'}>
<Suspense fallback={<div>Loading...</div>}>
<Admintools />
</Suspense>

View File

@ -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);
}

406
src/core/adminfunctions.js Normal file
View File

@ -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');
}
}

View File

@ -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<Cell> {
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<Cell> {
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

View File

@ -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;

View File

@ -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));
},

View File

@ -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<boolean> {
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<number> {
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,
};
}
}

View File

@ -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,

View File

@ -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);
}