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 * @flow
*/ */
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import type { State } from '../reducers'; import type { State } from '../reducers';
@ -96,10 +96,60 @@ async function submitIPAction(
callback(await resp.text()); 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({ function Admintools({
canvasId, canvasId,
canvases, canvases,
userlvl,
}) { }) {
const curDate = new Date(); const curDate = new Date();
let day = curDate.getDate(); let day = curDate.getDate();
@ -118,7 +168,9 @@ function Admintools({
const [brcoords, selectBRCoords] = useState(keptState.brcoords); const [brcoords, selectBRCoords] = useState(keptState.brcoords);
const [tlrcoords, selectTLRCoords] = useState(keptState.tlrcoords); const [tlrcoords, selectTLRCoords] = useState(keptState.tlrcoords);
const [brrcoords, selectBRRCoords] = useState(keptState.brrcoords); const [brrcoords, selectBRRCoords] = useState(keptState.brrcoords);
const [modName, selectModName] = useState(null);
const [resp, setResp] = useState(null); const [resp, setResp] = useState(null);
const [modlist, setModList] = useState([]);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
let descAction; let descAction;
@ -136,6 +188,12 @@ function Admintools({
// nothing // nothing
} }
useEffect(() => {
if (userlvl) {
getModList((mods) => setModList(mods));
}
}, []);
return ( return (
<p style={{ textAlign: 'center', paddingLeft: '5%', paddingRight: '5%' }}> <p style={{ textAlign: 'center', paddingLeft: '5%', paddingRight: '5%' }}>
{resp && ( {resp && (
@ -403,52 +461,144 @@ function Admintools({
{(submitting) ? '...' : 'Submit'} {(submitting) ? '...' : 'Submit'}
</button> </button>
<br /> {(userlvl === 1) && (
<div className="modaldivider" /> <div>
<h3 className="modaltitle">IP Actions</h3> <br />
<p className="modalcotext">Do stuff with IPs (one IP per line)</p> <div className="modaldivider" />
<select <h3 className="modaltitle">IP Actions</h3>
onChange={(e) => { <p className="modalcotext">Do stuff with IPs (one IP per line)</p>
const sel = e.target; <select
selectIPAction(sel.options[sel.selectedIndex].value); onChange={(e) => {
}} const sel = e.target;
> selectIPAction(sel.options[sel.selectedIndex].value);
{['ban', 'unban', 'whitelist', 'unwhitelist'].map((opt) => ( }}
<option
value={opt}
selected={iPAction === opt}
> >
{opt} {['ban', 'unban', 'whitelist', 'unwhitelist'].map((opt) => (
</option> <option
))} value={opt}
</select> selected={iPAction === opt}
<br /> >
<textarea rows="10" cols="17" id="iparea" /><br /> {opt}
<button </option>
type="button" ))}
onClick={() => { </select>
if (submitting) { <br />
return; <textarea rows="10" cols="17" id="iparea" /><br />
} <button
setSubmitting(true); type="button"
submitIPAction( onClick={() => {
iPAction, if (submitting) {
(ret) => { return;
setSubmitting(false); }
setResp(ret); setSubmitting(true);
}, submitIPAction(
); iPAction,
}} (ret) => {
> setSubmitting(false);
{(submitting) ? '...' : 'Submit'} setResp(ret);
</button> },
);
}}
>
{(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> </p>
); );
} }
function mapStateToProps(state: State) { function mapStateToProps(state: State) {
const { canvasId, canvases } = state.canvas; const { canvasId, canvases } = state.canvas;
return { canvasId, canvases }; const { userlvl } = state.user;
return { canvasId, canvases, userlvl };
} }
export default connect(mapStateToProps)(Admintools); export default connect(mapStateToProps)(Admintools);

View File

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

View File

@ -223,7 +223,7 @@ export class ChatProvider {
const { id } = user; const { id } = user;
const name = user.getName(); const name = user.getName();
if (!user.isAdmin() && await cheapDetector(user.ip)) { if (!user.userlvl && await cheapDetector(user.ip)) {
logger.info( logger.info(
`${name} / ${user.ip} tried to send chat message with proxy`, `${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.'; 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); 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); const setColor = await RedisCanvas.getPixelByOffset(canvasId, i, j, offset);
if (setColor & 0x80 if (setColor & 0x80
/* 3D Canvas Minecraft Avatars */ /* 3D Canvas Minecraft Avatars */
// && x >= 96 && x <= 128 && z >= 35 && z <= 100 // && x >= 96 && x <= 128 && z >= 35 && z <= 100
// 96 - 128 on x // 96 - 128 on x
// 32 - 128 on z // 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 // protected pixel
throw new Error(8); throw new Error(8);
} }
coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd; coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd;
if (user.isAdmin()) { if (isAdmin) {
coolDown = 0.0; coolDown = 0.0;
} else if (rpgEvent.success) { } else if (rpgEvent.success) {
if (rpgEvent.success === 1) { 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); const setColor = await RedisCanvas.getPixel(canvasId, x, y, z);
let coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd; let coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd;
if (user.isAdmin()) { if (isAdmin) {
coolDown = 0.0; coolDown = 0.0;
} else if (rpgEvent.success) { } else if (rpgEvent.success) {
if (rpgEvent.success === 1) { if (rpgEvent.success === 1) {
@ -293,7 +296,7 @@ export async function drawByCoords(
if (setColor & 0x80 if (setColor & 0x80
|| (canvas.v || (canvas.v
&& x >= 96 && x <= 128 && z >= 35 && z <= 100 && x >= 96 && x <= 128 && z >= 35 && z <= 100
&& !user.isAdmin()) && !isAdmin)
) { ) {
logger.info(`${user.ip} tried to set on protected pixel (${x}, ${y})`); logger.info(`${user.ip} tried to set on protected pixel (${x}, ${y})`);
return { return {
@ -338,10 +341,6 @@ export function drawSafeByCoords(
y: number, y: number,
z: number = null, z: number = null,
): Promise<Cell> { ): Promise<Cell> {
if (user.isAdmin()) {
return drawByCoords(user, canvasId, color, x, y, z);
}
// can just check for one unique occurence, // can just check for one unique occurence,
// we use ip, because id for logged out users is // we use ip, because id for logged out users is
// always null // always null
@ -379,10 +378,6 @@ export function drawSafeByOffset(
j: number, j: number,
offset: number, offset: number,
): Promise<Cell> { ): Promise<Cell> {
if (user.isAdmin()) {
return drawByOffset(user, canvasId, color, i, j, offset);
}
// can just check for one unique occurence, // can just check for one unique occurence,
// we use ip, because id for logged out users is // we use ip, because id for logged out users is
// always null // 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; export default logger;

View File

@ -29,6 +29,13 @@ const RegUser = Model.define('User', {
allowNull: false, allowNull: false,
}, },
// currently just moderator
roles: {
type: DataType.TINYINT,
allowNull: false,
defaultValue: 0,
},
// null if external oauth authentification // null if external oauth authentification
password: { password: {
type: DataType.CHAR(60), type: DataType.CHAR(60),
@ -125,6 +132,10 @@ const RegUser = Model.define('User', {
blockDm(): boolean { blockDm(): boolean {
return this.blocks & 0x01; return this.blocks & 0x01;
}, },
isMod(): boolean {
return this.roles & 0x01;
},
}, },
setterMethods: { setterMethods: {
@ -143,6 +154,11 @@ const RegUser = Model.define('User', {
this.setDataValue('blocks', val); this.setDataValue('blocks', val);
}, },
isMod(num: boolean) {
const val = (num) ? (this.roles | 0x01) : (this.roles & ~0x01);
this.setDataValue('roles', val);
},
password(value: string) { password(value: string) {
if (value) this.setDataValue('password', generateHash(value)); if (value) this.setDataValue('password', generateHash(value));
}, },

View File

@ -22,7 +22,14 @@ class User {
ip: string; ip: string;
wait: ?number; wait: ?number;
regUser: Object; 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') { constructor(id: string = null, ip: string = '127.0.0.1') {
// id should stay null if unregistered // id should stay null if unregistered
@ -30,6 +37,7 @@ class User {
this.ip = ip; this.ip = ip;
this.channels = {}; this.channels = {};
this.blocked = []; this.blocked = [];
this.userlvl = 0;
this.ipSub = getIPv6Subnet(ip); this.ipSub = getIPv6Subnet(ip);
this.wait = null; this.wait = null;
// following gets populated by passport // following gets populated by passport
@ -54,6 +62,14 @@ class User {
setRegUser(reguser) { setRegUser(reguser) {
this.regUser = reguser; this.regUser = reguser;
this.id = reguser.id; this.id = reguser.id;
if (this.regUser.isMod) {
this.userlvl = 2;
}
if (ADMIN_IDS.includes(this.id)) {
this.userlvl = 1;
}
if (reguser.channel) { if (reguser.channel) {
for (let i = 0; i < reguser.channel.length; i += 1) { for (let i = 0; i < reguser.channel.length; i += 1) {
const { const {
@ -139,7 +155,7 @@ class User {
async incrementPixelcount(): Promise<boolean> { async incrementPixelcount(): Promise<boolean> {
const { id } = this; const { id } = this;
if (!id) return false; if (!id) return false;
if (this.isAdmin()) return false; if (this.userlvl === 1) return false;
try { try {
await RegUser.update({ await RegUser.update({
totalPixels: Sequelize.literal('totalPixels + 1'), totalPixels: Sequelize.literal('totalPixels + 1'),
@ -156,7 +172,7 @@ class User {
async getTotalPixels(): Promise<number> { async getTotalPixels(): Promise<number> {
const { id } = this; const { id } = this;
if (!id) return 0; if (!id) return 0;
if (this.isAdmin()) return 100000; if (this.userlvl === 1) return 100000;
if (this.regUser) { if (this.regUser) {
return this.regUser.totalPixels; return this.regUser.totalPixels;
} }
@ -196,10 +212,6 @@ class User {
return true; return true;
} }
isAdmin(): boolean {
return ADMIN_IDS.includes(this.id);
}
getUserData(): Object { getUserData(): Object {
if (this.regUser == null) { if (this.regUser == null) {
return { return {
@ -218,7 +230,9 @@ class User {
blocked: this.blocked, blocked: this.blocked,
}; };
} }
const { regUser } = this; const {
regUser, userlvl, channels, blocked,
} = this;
return { return {
name: regUser.name, name: regUser.name,
mailVerified: regUser.mailVerified, mailVerified: regUser.mailVerified,
@ -230,9 +244,9 @@ class User {
ranking: regUser.ranking, ranking: regUser.ranking,
dailyRanking: regUser.dailyRanking, dailyRanking: regUser.dailyRanking,
mailreg: !!(regUser.password), mailreg: !!(regUser.password),
userlvl: this.isAdmin() ? 1 : 0, userlvl,
channels: this.channels, channels,
blocked: this.blocked, blocked,
}; };
} }
} }

View File

@ -33,7 +33,7 @@ export type UserState = {
isOnMobile: boolean, isOnMobile: boolean,
// small notifications for received cooldown // small notifications for received cooldown
notification: string, notification: string,
// 1: Admin, 0: ordinary user // 1: Admin, 2: Mod, 0: ordinary user
userlvl: number, userlvl: number,
// regExp for detecting ping // regExp for detecting ping
nameRegExp: RegExp, nameRegExp: RegExp,

View File

@ -6,30 +6,27 @@
* *
*/ */
/* eslint-disable no-await-in-loop */
import express from 'express'; import express from 'express';
import expressLimiter from 'express-limiter'; import expressLimiter from 'express-limiter';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import sharp from 'sharp';
import multer from 'multer'; import multer from 'multer';
import { getIPFromRequest, getIPv6Subnet } from '../utils/ip'; import { getIPFromRequest } from '../utils/ip';
import redis from '../data/redis'; import redis from '../data/redis';
import session from '../core/session'; import session from '../core/session';
import passport from '../core/passport'; import passport from '../core/passport';
import logger from '../core/logger'; import { admintoolsLogger } from '../core/logger';
import { Blacklist, Whitelist } from '../data/models';
import { MINUTE } from '../core/constants'; import { MINUTE } from '../core/constants';
// eslint-disable-next-line import/no-unresolved
import canvases from './canvases.json';
import { import {
imageABGR2Canvas, executeIPAction,
protectCanvasArea, executeImageAction,
} from '../core/Image'; executeProtAction,
import rollbackCanvasArea from '../core/rollback'; executeRollback,
getModList,
removeMod,
makeMod,
} from '../core/adminfunctions';
const router = express.Router(); const router = express.Router();
@ -50,6 +47,7 @@ const upload = multer({
/* /*
* rate limiting to prevent bruteforce attacks * rate limiting to prevent bruteforce attacks
* TODO: do that with nginx
*/ */
router.use('/', router.use('/',
limiter({ 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(session);
router.use(passport.initialize()); router.use(passport.initialize());
@ -69,350 +67,31 @@ router.use(passport.session());
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
const ip = getIPFromRequest(req); const ip = getIPFromRequest(req);
if (!req.user) { 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'); res.status(403).send('You are not logged in');
return; 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`, `ADMINTOOLS: ${ip} / ${req.user.id} tried to access admintools`,
); );
res.status(403).send('You are not allowed to access this page'); res.status(403).send('You are not allowed to access this page');
return; return;
} }
logger.info( admintoolsLogger.info(
`ADMINTOOLS: ${req.user.id} / ${req.user.regUser.name} is using admintools`, `ADMINTOOLS: ${req.user.id} / ${req.user.regUser.name} is using admintools`,
); );
next(); next();
}); });
/* /*
* Execute IP based actions (banning, whitelist, etc.) * Post for mod + admin
* @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,
*/ */
router.post('/', upload.single('image'), async (req, res, next) => { router.post('/', upload.single('image'), async (req, res, next) => {
try { try {
@ -426,10 +105,6 @@ router.post('/', upload.single('image'), async (req, res, next) => {
); );
res.status(ret).send(msg); res.status(ret).send(msg);
return; 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) { } if (req.body.protaction) {
const { const {
protaction, ulcoor, brcoor, canvasid, 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 { try {
const { ip, ipaction } = req.query; if (req.body.ipaction) {
if (!ipaction) { const ret = await executeIPAction(req.body.ipaction, req.body.ip);
next(); res.status(200).send(ret);
return; return;
} }
if (!ip) { if (req.body.modlist) {
res.status(400).json({ errors: 'invalid ip' }); const ret = await getModList();
res.status(200);
res.json(ret);
return; return;
} }
if (req.body.remmod) {
const ret = await executeIPAction(ipaction, ip); try {
const ret = await removeMod(req.body.remmod);
res.json({ ipaction: 'success', messages: ret.split('\n') }); 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) { } catch (error) {
next(error); next(error);
} }