add admintools to user area

This commit is contained in:
HF 2020-06-22 15:34:59 +02:00
parent bc7613b431
commit 1d081991ba
10 changed files with 367 additions and 142 deletions

View File

@ -492,6 +492,7 @@ export function receiveMe(
dailyRanking,
minecraftname,
canvases,
userlvl,
} = me;
return {
type: 'RECEIVE_ME',
@ -504,6 +505,7 @@ export function receiveMe(
dailyRanking,
minecraftname,
canvases,
userlvl,
};
}

View File

@ -79,7 +79,8 @@ export type Action =
ranking: number,
dailyRanking: number,
minecraftname: string,
canvases: Object
canvases: Object,
userlvl: number,
}
| { type: 'RECEIVE_STATS', totalRanking: Object, totalDailyRanking: Object }
| { type: 'SET_NAME', name: string }

View File

@ -1,57 +0,0 @@
/*
* Html for adminpage
*
* @flow
*/
import React from 'react';
import ReactDOM from 'react-dom/server';
import Html from './Html';
const Admin = () => (
<form method="post" action="admintools" encType="multipart/form-data">
<p>------Image Upload------</p>
<p>file:</p>
<select name="imageaction">
<option value="build">build</option>
<option value="protect">protect</option>
<option value="wipe">wipe</option>
</select>
<input type="file" name="image" /><br />
<p><span>canvasId (d: default, m: moon):</span>
<input type="text" name="canvasident" /></p>
<span>x:</span>
<input type="number" name="x" min="-40000" max="40000" /><br />
<span>y:</span>
<input type="number" name="y" min="-40000" max="40000" /><br />
<br />
<p>---------IP actions---------</p>
<select name="action">
<option value="ban">ban</option>
<option value="unban">unban</option>
<option value="whitelist">whitelist</option>
<option value="unwhitelist">unwhitelist</option>
</select>
<br />
<textarea rows="10" cols="100" name="ip" /><br />
<p>-----------------------</p>
<button type="submit" name="upload">Submit</button>
</form>
);
const title = 'PixelPlanet.fun AdminTools';
const description = 'admin access on pixelplanet';
const body = <Admin />;
const adminHtml = `<!doctype html>${
ReactDOM.renderToStaticMarkup(
<Html
title={title}
description={description}
body={body}
/>,
)
}`;
export default adminHtml;

View File

@ -0,0 +1,232 @@
/*
* Html for adminpage
*
* @flow
*/
import React, { useState } from 'react';
import { connect } from 'react-redux';
import type { State } from '../reducers';
async function submitAction(
action,
canvas,
coords,
callback,
) {
const data = new FormData();
const fileSel = document.getElementById('imgfile');
const file = (!fileSel.files || !fileSel.files[0])
? null : fileSel.files[0];
data.append('imageaction', action);
data.append('image', file);
data.append('canvasid', canvas);
data.append('coords', coords);
const resp = await fetch('./admintools', {
credentials: 'include',
method: 'POST',
body: data,
});
callback(await resp.text());
}
async function submitIPAction(
action,
callback,
) {
const data = new FormData();
const iplist = document.getElementById('iparea').value;
data.append('ip', iplist);
data.append('ipaction', action);
const resp = await fetch('./admintools', {
credentials: 'include',
method: 'POST',
body: data,
});
callback(await resp.text());
}
function Admintools({
canvasId,
canvases,
}) {
const [selectedCanvas, selectCanvas] = useState(canvasId);
const [imageAction, selectImageAction] = useState('build');
const [iPAction, selectIPAction] = useState('ban');
const [resp, setResp] = useState(null);
const [coords, selectCoords] = useState('X_Y');
const [submitting, setSubmitting] = useState(false);
let descAction;
switch (imageAction) {
case 'build':
descAction = 'Build image on canvas.';
break;
case 'protect':
descAction = 'Build image and set it to protected.';
break;
case 'wipe':
descAction = 'Build image, but reset cooldown to unset-pixel cd.';
break;
default:
// nothing
}
return (
<p style={{ textAlign: 'center', paddingLeft: '5%', paddingRight: '5%' }}>
{resp && (
<div style={{
borderStyle: 'solid',
borderColor: '#D4D4D4',
borderWidth: 2,
padding: 5,
display: 'inline-block',
}}
>
{resp.split('\n').map((line) => (
<p className="modaltext">
{line}
</p>
))}
<span
role="button"
tabIndex={-1}
className="modallink"
onClick={() => setResp(null)}
>
Close
</span>
</div>
)}
<h3 className="modaltitle">Image Upload</h3>
<p className="modalcotext">Upload images to canvas</p>
<p className="modalcotext">Choose Canvas:&nbsp;
<select
onChange={(e) => {
const sel = e.target;
selectCanvas(sel.options[sel.selectedIndex].value);
}}
>
{
Object.keys(canvases).map((canvas) => ((canvases[canvas].v)
? null
: (
<option
selected={canvas === selectedCanvas}
value={canvas}
>
{
canvases[canvas].title
}
</option>
)))
}
</select>
</p>
<p className="modalcotext">
File:&nbsp;
<input type="file" name="image" id="imgfile" />
</p>
<select
onChange={(e) => {
const sel = e.target;
selectImageAction(sel.options[sel.selectedIndex].value);
}}
>
{['build', 'protect', 'wipe'].map((opt) => (
<option
value={opt}
selected={imageAction === opt}
>
{opt}
</option>
))}
</select>
<p className="modalcotext">{descAction}</p>
<p className="modalcotext">
Coordinates in X_Y format:&nbsp;
<input
value={coords}
style={{
display: 'inline-block',
width: '100%',
maxWidth: '15em',
}}
type="text"
onChange={(evt) => {
selectCoords(evt.target.value.trim());
}}
/>
</p>
<button
type="button"
onClick={() => {
if (submitting) {
return;
}
setSubmitting(true);
submitAction(
imageAction,
selectedCanvas,
coords,
(ret) => {
setSubmitting(false);
setResp(ret);
},
);
}}
>
{(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}
>
{opt}
</option>
))}
</select>
<br />
<textarea rows="10" cols="100" id="iparea" /><br />
<button
type="button"
onClick={() => {
if (submitting) {
return;
}
setSubmitting(true);
submitIPAction(
iPAction,
(ret) => {
setSubmitting(false);
setResp(ret);
},
);
}}
>
{(submitting) ? '...' : 'Submit'}
</button>
</p>
);
}
function mapStateToProps(state: State) {
const { canvasId, canvases } = state.canvas;
return { canvasId, canvases };
}
export default connect(mapStateToProps)(Admintools);

View File

@ -31,6 +31,9 @@ class Tabs extends Component {
<div className="tabs">
<ol className="tab-list">
{children.map((child) => {
if (!child.props) {
return undefined;
}
const { label } = child.props;
return (
@ -45,7 +48,9 @@ class Tabs extends Component {
</ol>
<div className="tab-content">
{children.map((child) => {
if (child.props.label !== activeTab) return undefined;
if (!child.props || child.props.label !== activeTab) {
return undefined;
}
return child.props.children;
})}
</div>

View File

@ -19,6 +19,8 @@ import Rankings from './Rankings';
// eslint-disable-next-line max-len
const Converter = React.lazy(() => import(/* webpackChunkName: "converter" */ './Converter'));
// eslint-disable-next-line max-len
const Admintools = React.lazy(() => import(/* webpackChunkName: "admintools" */ './Admintools'));
const logoStyle = {
marginRight: 5,
@ -82,7 +84,14 @@ const LogInArea = ({ register, forgotPassword, me }) => (
);
const UserAreaModal = ({
name, register, forgotPassword, doMe, logout, setUserName, setUserMailreg,
name,
register,
forgotPassword,
doMe,
logout,
setUserName,
setUserMailreg,
userlvl,
}) => (
<p style={{ textAlign: 'center' }}>
{(name === null)
@ -110,6 +119,13 @@ const UserAreaModal = ({
<Converter />
</Suspense>
</div>
{userlvl && (
<div label="Admintools">
<Suspense fallback={<div>Loading...</div>}>
<Admintools />
</Suspense>
</div>
)}
</Tabs>
)}
<p>Also join our Discord:&nbsp;
@ -149,8 +165,8 @@ function mapDispatchToProps(dispatch) {
}
function mapStateToProps(state: State) {
const { name } = state.user;
return { name };
const { name, userlvl } = state.user;
return { name, userlvl };
}
const data = {

View File

@ -48,6 +48,7 @@ export async function imageABGR2Canvas(
const [ucx, ucy] = getChunkOfPixel(size, x, y);
const [lcx, lcy] = getChunkOfPixel(size, x + width, y + height);
let totalPxlCnt = 0;
logger.info(`Loading to chunks from ${ucx} / ${ucy} to ${lcx} / ${lcy} ...`);
let chunk;
for (let cx = ucx; cx <= lcx; cx += 1) {
@ -83,12 +84,14 @@ export async function imageABGR2Canvas(
const ret = await RedisCanvas.setChunk(cx, cy, chunk, canvasId);
if (ret) {
logger.info(`Loaded ${pxlCnt} pixels into chunk ${cx}, ${cy}.`);
totalPxlCnt += pxlCnt;
}
}
chunk = null;
}
}
logger.info('Image loading done.');
return totalPxlCnt;
}

View File

@ -148,6 +148,7 @@ class User {
ranking: regUser.ranking,
dailyRanking: regUser.dailyRanking,
mailreg: !!(regUser.password),
userlvl: this.isAdmin() ? 1 : 0,
};
}
}

View File

@ -32,6 +32,8 @@ export type UserState = {
isOnMobile: boolean,
// small notifications for received cooldown
notification: string,
// 1: Admin, 0: ordinary user
userlvl: number,
};
const initialState: UserState = {
@ -53,6 +55,7 @@ const initialState: UserState = {
minecraftname: null,
isOnMobile: false,
notification: null,
userlvl: 0,
};
export default function user(
@ -171,6 +174,7 @@ export default function user(
ranking,
dailyRanking,
minecraftname,
userlvl,
} = action;
const messages = (action.messages) ? action.messages : [];
return {
@ -183,6 +187,7 @@ export default function user(
ranking,
dailyRanking,
minecraftname,
userlvl,
};
}

View File

@ -1,5 +1,6 @@
/**
* basic admin api
* is used by ../components/Admintools
*
* @flow
*
@ -15,7 +16,6 @@ import sharp from 'sharp';
import multer from 'multer';
import { getIPFromRequest, getIPv6Subnet } from '../utils/ip';
import { getIdFromObject } from '../core/utils';
import redis from '../data/redis';
import session from '../core/session';
import passport from '../core/passport';
@ -27,8 +27,6 @@ import { MINUTE } from '../core/constants';
import canvases from './canvases.json';
import { imageABGR2Canvas } from '../core/Image';
import adminHtml from '../components/Admin';
const router = express.Router();
const limiter = expressLimiter(router, redis);
@ -73,7 +71,7 @@ router.use(async (req, res, next) => {
}
if (!req.user.isAdmin()) {
logger.info(
`ADMINTOOLS: ${ip}/${req.user.id} wrongfully 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');
return;
@ -91,7 +89,7 @@ router.use(async (req, res, next) => {
* @param ip already sanizized ip
* @return true if successful
*/
async function executeAction(action: string, ips: string): boolean {
async function executeIPAction(action: string, ips: string): boolean {
const ipArray = ips.split('\n');
let out = '';
const splitRegExp = /\s+/;
@ -104,7 +102,7 @@ async function executeAction(action: string, ips: string): boolean {
ip = ipLine[2];
}
if (!ip || ip.length < 8 || ip.indexOf(' ') !== -1) {
out += `Couln't parse ${action} ${ip}<br>\n`;
out += `Couln't parse ${action} ${ip}\n`;
continue;
}
const ipKey = getIPv6Subnet(ip);
@ -137,84 +135,106 @@ async function executeAction(action: string, ips: string): boolean {
await redis.del(key);
break;
default:
out += `Failed to ${action} ${ip}<br>\n`;
out += `Failed to ${action} ${ip}\n`;
}
out += `Succseefully did ${action} ${ip}<br>\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
* @return [ret, msg] http status code and message
*/
async function executeImageAction(
action: string,
file: Object,
coords: string,
canvasid: string,
) {
const splitCoords = coords.trim().split('_');
if (splitCoords.length !== 2) {
return [403, 'Invalid Coordinate Format'];
}
const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
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 (!canvasid) {
error = 'No canvas specified';
} else if (!canvases[canvasid]) {
error = 'Invalid canvas selected';
}
if (error !== null) {
return [403, error];
}
const canvas = canvases[canvasid];
if (canvas.v) {
return [403, 'Can not upload Image to 3D canvas'];
}
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,
);
return [
200,
`Successfully loaded image wth ${pxlCount} pixels to ${x}/${y}`,
];
} catch {
return [400, 'Can not read image file'];
}
}
/*
* Check for POST parameters,
*/
router.post('/', upload.single('image'), async (req, res, next) => {
try {
if (req.file) {
const { imageaction, canvasident } = req.body;
let error = null;
if (Number.isNaN(req.body.x)) {
error = 'x is not a valid number';
} else if (Number.isNaN(req.body.y)) {
error = 'y is not a valid number';
} else if (!imageaction) {
error = 'No imageaction given';
} else if (!canvasident) {
error = 'No canvas specified';
}
if (error !== null) {
res.status(403).send(error);
return;
}
const x = parseInt(req.body.x, 10);
const y = parseInt(req.body.y, 10);
const canvasId = getIdFromObject(canvases, canvasident);
if (canvasId === null) {
res.status(403).send('This canvas does not exist');
return;
}
const canvas = canvases[canvasId];
if (canvas.v) {
res.status(403).send('Can not upload Image to 3D canvas');
return;
}
const canvasMaxXY = canvas.size / 2;
const canvasMinXY = -canvasMaxXY;
if (x < canvasMinXY || y < canvasMinXY
|| x >= canvasMaxXY || y >= canvasMaxXY) {
res.status(403).send('Coordinates are outside of canvas');
return;
}
const protect = (imageaction === 'protect');
const wipe = (imageaction === 'wipe');
await sharp(req.file.buffer)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true })
.then(({ err, data, info }) => {
if (err) throw err;
return imageABGR2Canvas(
canvasId,
x, y,
data,
info.width, info.height,
wipe, protect,
);
});
res.status(200).send('Successfully loaded image');
if (req.body.imageaction) {
const { imageaction, coords, canvasid } = req.body;
const [ret, msg] = await executeImageAction(
imageaction,
req.file,
coords,
canvasid,
);
res.status(ret).send(msg);
return;
}
if (req.body.ip) {
const ret = await executeAction(req.body.action, req.body.ip);
} if (req.body.ipaction) {
const ret = await executeIPAction(req.body.ipaction, req.body.ip);
res.status(200).send(ret);
return;
}
@ -231,8 +251,8 @@ router.post('/', upload.single('image'), async (req, res, next) => {
*/
router.get('/', async (req: Request, res: Response, next) => {
try {
const { ip, action } = req.query;
if (!action) {
const { ip, ipaction } = req.query;
if (!ipaction) {
next();
return;
}
@ -241,9 +261,9 @@ router.get('/', async (req: Request, res: Response, next) => {
return;
}
const ret = await executeAction(action, ip);
const ret = await executeIPAction(ipaction, ip);
res.json({ action: 'success', messages: ret.split('\n') });
res.json({ ipaction: 'success', messages: ret.split('\n') });
} catch (error) {
next(error);
}
@ -251,10 +271,7 @@ router.get('/', async (req: Request, res: Response, next) => {
router.use(async (req: Request, res: Response) => {
res.set({
'Content-Type': 'text/html',
});
res.status(200).send(adminHtml);
res.status(400).send('Invalid request');
});