create Moderator role
This commit is contained in:
parent
a9922e3041
commit
9a7ca41eb9
|
@ -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:
|
||||||
|
<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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
406
src/core/adminfunctions.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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));
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user