Horicontal Scaling

This commit is contained in:
HF 2022-09-10 00:35:28 +02:00
parent ae6ebc2441
commit de1729d56b
55 changed files with 936 additions and 341 deletions

View File

@ -5,6 +5,8 @@
import React, { useState, useEffect } from 'react';
import { t } from 'ttag';
import { shardOrigin } from '../store/actions/fetch';
async function submitIPAction(
action,
vallist,
@ -13,7 +15,7 @@ async function submitIPAction(
const data = new FormData();
data.append('ipaction', action);
data.append('ip', vallist);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,
@ -26,7 +28,7 @@ async function getModList(
) {
const data = new FormData();
data.append('modlist', true);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,
@ -44,7 +46,7 @@ async function submitRemMod(
) {
const data = new FormData();
data.append('remmod', userId);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,
@ -58,7 +60,7 @@ async function submitMakeMod(
) {
const data = new FormData();
data.append('makemod', userName);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,

View File

@ -10,9 +10,10 @@ import React, { useState, useEffect } from 'react';
import { t } from 'ttag';
import { IoReloadCircleSharp } from 'react-icons/io5';
import { shardOrigin } from '../store/actions/fetch';
async function getUrlAndId() {
const url = './captcha.svg';
const url = `${shardOrigin}/captcha.svg`;
const resp = await fetch(url, {
cache: 'no-cache',
});

View File

@ -66,7 +66,14 @@ function LanguageSelect() {
/* set with selected language */
const d = new Date();
d.setTime(d.getTime() + 24 * MONTH);
document.cookie = `lang=${langSel};expires=${d.toUTCString()};path=/`;
let { host } = window.location;
if (host.lastIndexOf('.') !== host.indexOf('.')) {
host = host.slice(host.indexOf('.'));
} else {
host = `.${host}`;
}
// eslint-disable-next-line max-len
document.cookie = `lang=${langSel};expires=${d.toUTCString()};path=/;domain=${host}`;
window.location.reload();
}}
>

View File

@ -8,6 +8,7 @@ import { t } from 'ttag';
import useInterval from './hooks/interval';
import { getToday, dateToString } from '../core/utils';
import { shardOrigin } from '../store/actions/fetch';
const keptState = {
coords: '',
@ -33,7 +34,7 @@ async function submitImageAction(
data.append('image', file);
data.append('canvasid', canvas);
data.append('coords', coords);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,
@ -53,7 +54,7 @@ async function submitProtAction(
data.append('canvasid', canvas);
data.append('ulcoor', tlcoords);
data.append('brcoor', brcoords);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,
@ -74,7 +75,7 @@ async function submitRollback(
data.append('canvasid', canvas);
data.append('ulcoor', tlcoords);
data.append('brcoor', brcoords);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,
@ -94,7 +95,7 @@ async function submitCanvasCleaner(
data.append('canvasid', canvas);
data.append('ulcoor', tlcoords);
data.append('brcoor', brcoords);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,
@ -107,7 +108,7 @@ async function getCleanerStats(
) {
const data = new FormData();
data.append('cleanerstat', true);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,
@ -125,7 +126,7 @@ async function getCleanerCancel(
) {
const data = new FormData();
data.append('cleanercancel', true);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,

View File

@ -6,6 +6,7 @@ import React, { useState } from 'react';
import { t } from 'ttag';
import { parseInterval } from '../core/utils';
import { shardOrigin } from '../store/actions/fetch';
async function submitIIDAction(
action,
@ -31,7 +32,7 @@ async function submitIIDAction(
data.append('reason', reason);
data.append('time', time);
data.append('iid', iid);
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,

View File

@ -9,6 +9,7 @@ import { t } from 'ttag';
import copyTextToClipboard from '../utils/clipboard';
import { parseInterval } from '../core/utils';
import { shardOrigin } from '../store/actions/fetch';
const keepState = {
tlcoords: '',
@ -54,7 +55,7 @@ async function submitWatchAction(
data.append('time', time);
data.append('iid', iid);
try {
const resp = await fetch('/api/modtools', {
const resp = await fetch(`${shardOrigin}/api/modtools`, {
credentials: 'include',
method: 'POST',
body: data,

View File

@ -10,7 +10,7 @@ import {
} from '../data/sql';
import { findIdByNameOrId } from '../data/sql/RegUser';
import ChatMessageBuffer from './ChatMessageBuffer';
import socketEvents from '../socket/SocketEvents';
import socketEvents from '../socket/socketEvents';
import checkIPAllowed from './isAllowed';
import { DailyCron } from '../utils/cron';
import { escapeMd } from './utils';
@ -93,6 +93,9 @@ export class ChatProvider {
}
async clearOldMessages() {
if (!socketEvents.amIImportant()) {
return;
}
const ids = Object.keys(this.defaultChannels);
for (let i = 0; i < ids.length; i += 1) {
const cid = ids[i];

View File

@ -4,20 +4,17 @@
/* eslint-disable max-len */
import { randomUUID } from 'crypto';
import nodemailer from 'nodemailer';
import logger from './logger';
import { HOUR, MINUTE } from './constants';
import { DailyCron, HourlyCron } from '../utils/cron';
import { getTTag } from './ttag';
import { codeExists, checkCode, setCode } from '../data/redis/mailCodes';
import socketEvents from '../socket/socketEvents';
import { USE_MAILER, MAIL_ADDRESS } from './config';
import { RegUser } from '../data/sql';
// TODO make code expire
class MailProvider {
export class MailProvider {
constructor() {
this.enabled = !!USE_MAILER;
if (this.enabled) {
@ -26,13 +23,23 @@ class MailProvider {
newline: 'unix',
path: '/usr/sbin/sendmail',
});
this.clearCodes = this.clearCodes.bind(this);
this.verifyCodes = {};
HourlyCron.hook(this.clearCodes);
DailyCron.hook(MailProvider.cleanUsers);
}
/*
* mail requests make it through SocketEvents when sharding
*/
socketEvents.on('mail', (type, args) => {
switch (type) {
case 'verify':
this.postVerifyMail(...args);
break;
case 'pwreset':
this.postPasswdResetMail(...args);
break;
default:
// nothing
}
});
}
sendMail(to, subject, html) {
@ -52,29 +59,10 @@ class MailProvider {
});
}
sendVerifyMail(to, name, host, lang) {
if (!this.enabled) {
return null;
}
postVerifyMail(to, name, host, lang, code) {
const { t } = getTTag(lang);
const pastMail = this.verifyCodes[to];
if (pastMail) {
const minLeft = Math.floor(
pastMail.timestamp / MINUTE + 2 - Date.now() / MINUTE,
);
if (minLeft > 0) {
logger.info(
`Verify mail for ${to} - already sent, ${minLeft} minutes left`,
);
return t`We already sent you a verification mail, you can request another one in ${minLeft} minutes.`;
}
}
logger.info(`Sending verification mail to ${to} / ${name}`);
const code = this.setCode(to);
const verifyUrl = `${host}/api/auth/verify?token=${code}`;
const verifyUrl = `${host}/api/auth/verify?token=${code}&email=${encodeURIComponent(to)}`;
const subject = t`Welcome ${name} to PixelPlanet, plese verify your mail`;
const html = `<em>${t`Hello ${name}`}</em>,<br />
${t`welcome to our little community of pixelplacers, to use your account, you have to verify your mail. You can do that here: `} <a href="${verifyUrl}">${t`Click to Verify`}</a>. ${t`Or by copying following url:`}<br />${verifyUrl}\n<br />
@ -82,27 +70,60 @@ class MailProvider {
${t`Thanks`}<br /><br />
<img alt="" src="https://assets.pixelplanet.fun/tile.png" style="height:64px; width:64px" />`;
this.sendMail(to, subject, html);
}
async sendVerifyMail(to, name, host, lang) {
if (!this.enabled && !socketEvents.isCluster) {
return null;
}
const { t } = getTTag(lang);
const pastCodeAge = await codeExists(to);
if (pastCodeAge && pastCodeAge < 180) {
const minLeft = Math.ceil((180 - pastCodeAge) / 60);
logger.info(
`Verify mail for ${to} - already sent, ${minLeft} minutes left`,
);
return t`We already sent you a verification mail, you can request another one in ${minLeft} minutes.`;
}
const code = setCode(to);
if (this.enabled) {
this.postVerifyMail(to, name, host, lang, code);
} else {
socketEvents.sendMail('verify', [to, name, host, lang, code]);
}
return null;
}
postPasswdResetMail(to, ip, host, lang, code) {
const { t } = getTTag(lang);
logger.info(`Sending Password reset mail to ${to}`);
const restoreUrl = `${host}/reset_password?token=${code}`;
const subject = t`You forgot your password for PixelPlanet? Get a new one here`;
const html = `<em>${t`Hello`}</em>,<br />
${t`You requested to get a new password. You can change your password within the next 30min here: `} <a href="${restoreUrl}">${t`Reset Password`}</a>. ${t`Or by copying following url:`}<br />${restoreUrl}\n<br />
${t`If you did not request this mail, please just ignore it (the ip that requested this mail was ${ip}).`}<br />
${t`Thanks`}<br /><br />\n<img alt="" src="https://assets.pixelplanet.fun/tile.png" style="height:64px; width:64px" />`;
this.sendMail(to, subject, html);
}
async sendPasswdResetMail(to, ip, host, lang) {
const { t } = getTTag(lang);
if (!this.enabled) {
if (!this.enabled && !socketEvents.isCluster) {
return t`Mail is not configured on the server`;
}
const pastMail = this.verifyCodes[to];
if (pastMail) {
if (Date.now() < pastMail.timestamp + 15 * MINUTE) {
logger.info(
`Password reset mail for ${to} requested by ${ip} - already sent`,
);
return t`We already sent you a mail with instructions. Please wait before requesting another mail.`;
}
const pastCodeAge = await codeExists(to);
if (pastCodeAge && pastCodeAge < 180) {
logger.info(
`Password reset mail for ${to} requested by ${ip} - already sent`,
);
return t`We already sent you a mail with instructions. Please wait before requesting another mail.`;
}
const reguser = await RegUser.findOne({ where: { email: to } });
if (pastMail || !reguser) {
if (!reguser) {
logger.info(
`Password reset mail for ${to} requested by ${ip} - mail not found`,
);
@ -119,68 +140,20 @@ class MailProvider {
}
*/
logger.info(`Sending Password reset mail to ${to}`);
const code = this.setCode(to);
const restoreUrl = `${host}/reset_password?token=${code}`;
const subject = t`You forgot your password for PixelPlanet? Get a new one here`;
const html = `<em>${t`Hello`}</em>,<br />
${t`You requested to get a new password. You can change your password within the next 30min here: `} <a href="${restoreUrl}">${t`Reset Password`}</a>. ${t`Or by copying following url:`}<br />${restoreUrl}\n<br />
${t`If you did not request this mail, please just ignore it (the ip that requested this mail was ${ip}).`}<br />
${t`Thanks`}<br /><br />\n<img alt="" src="https://assets.pixelplanet.fun/tile.png" style="height:64px; width:64px" />`;
this.sendMail(to, subject, html);
const code = setCode(to);
if (this.enabled) {
this.postPasswdResetMail(to, ip, host, lang, code);
} else {
socketEvents.sendMail('pwreset', [to, ip, host, lang, code]);
}
return null;
}
setCode(email) {
const code = MailProvider.createCode();
this.verifyCodes[email] = {
code,
timestamp: Date.now(),
};
return code;
}
async clearCodes() {
const curTime = Date.now();
const toDelete = [];
const mails = Object.keys(this.verifyCodes);
for (let i = 0; i < mails.length; i += 1) {
const iteremail = mails[i];
if (curTime > this.verifyCodes[iteremail].timestamp + HOUR) {
toDelete.push(iteremail);
}
}
toDelete.forEach((email) => {
logger.info(`Mail Code for ${email} expired`);
delete this.verifyCodes[email];
});
}
// Note: code gets deleted on check
checkCode(code) {
let email = null;
const mails = Object.keys(this.verifyCodes);
for (let i = 0; i < mails.length; i += 1) {
const iteremail = mails[i];
if (this.verifyCodes[iteremail].code === code) {
email = iteremail;
break;
}
}
if (!email) {
logger.info(`Mail Code ${code} not found.`);
static async verify(email, code) {
const ret = await checkCode(email, code);
if (!ret) {
return false;
}
logger.info(`Got Mail Code from ${email}.`);
delete this.verifyCodes[email];
return email;
}
async verify(code) {
const email = this.checkCode(code);
if (!email) return false;
const reguser = await RegUser.findOne({ where: { email } });
if (!reguser) {
logger.error(`${email} does not exist in database`);
@ -193,13 +166,10 @@ class MailProvider {
return reguser.name;
}
static createCode() {
return randomUUID();
}
/*
* we do not use this right now
static cleanUsers() {
// delete users that requier verification for more than 4 days
/*
RegUser.destroy({
where: {
verificationReqAt: {
@ -209,8 +179,8 @@ class MailProvider {
verified: 0,
},
});
*/
}
*/
}
const mailProvider = new MailProvider();

View File

@ -3,7 +3,7 @@
* in bursts per chunk
*/
import socketEvents from '../socket/SocketEvents';
import socketEvents from '../socket/socketEvents';
class PixelCache {
PXL_CACHE;

View File

@ -242,6 +242,14 @@ function addIndexedSubtiletoTile(
function tileFileName(canvasTileFolder, cell) {
const [z, x, y] = cell;
const filename = `${canvasTileFolder}/${z}/${x}/${y}.webp`;
try {
const mtime = new Date(fs.statSync(filename).mtime).getTime();
if (Date.now() - mtime < 120000) {
return null;
}
} catch {
// file doesn't exist
}
return filename;
}
@ -263,6 +271,10 @@ export async function createZoomTileFromChunk(
const canvasSize = canvas.size;
const [x, y] = cell;
const maxTiledZoom = getMaxTiledZoom(canvasSize);
const filename = tileFileName(canvasTileFolder, [maxTiledZoom - 1, x, y]);
if (!filename) return true;
const tileRGBBuffer = new Uint8Array(
TILE_SIZE * TILE_SIZE * TILE_ZOOM_LEVEL * TILE_ZOOM_LEVEL * 3,
);
@ -318,7 +330,6 @@ export async function createZoomTileFromChunk(
);
});
const filename = tileFileName(canvasTileFolder, [maxTiledZoom - 1, x, y]);
try {
await sharp(tileRGBBuffer, {
raw: {
@ -363,6 +374,9 @@ export async function createZoomedTile(
);
const [z, x, y] = cell;
const filename = tileFileName(canvasTileFolder, [z, x, y]);
if (!filename) return true;
const startTime = Date.now();
const na = [];
@ -409,7 +423,6 @@ export async function createZoomedTile(
);
});
const filename = tileFileName(canvasTileFolder, [z, x, y]);
try {
await sharp(tileRGBBuffer, {
raw: {

View File

@ -5,7 +5,7 @@
* if it reaches the TARGET_RADIUS size, the event is lost
*
*/
import socketEvents from '../socket/SocketEvents';
import socketEvents from '../socket/socketEvents';
import PixelUpdate from '../socket/packets/PixelUpdateServer';
import { setPixelByOffset } from './setPixel';
import { TILE_SIZE } from './constants';

View File

@ -20,7 +20,7 @@ export const MAIL_ADDRESS = process.env.MAIL_ADDRESS
const TILE_FOLDER_REL = process.env.TILE_FOLDER || 'tiles';
export const TILE_FOLDER = path.join(__dirname, `./${TILE_FOLDER_REL}`);
export const USE_XREALIP = process.env.USE_XREALIP || false;
export const USE_XREALIP = !!process.env.USE_XREALIP;
export const BACKUP_URL = process.env.BACKUP_URL || null;
export const BACKUP_DIR = process.env.BACKUP_DIR || null;
@ -30,6 +30,7 @@ export const USE_PROXYCHECK = parseInt(process.env.USE_PROXYCHECK, 10) || false;
export const { PROXYCHECK_KEY } = process.env;
export const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
export const SHARD_NAME = process.env.SHARD_NAME || null;
// Database
export const MYSQL_HOST = process.env.MYSQL_HOST || 'localhost';
export const MYSQL_DATABASE = process.env.MYSQL_DATABASE || 'pixelplanet';
@ -52,6 +53,9 @@ export const APISOCKET_KEY = process.env.APISOCKET_KEY || null;
export const ADMIN_IDS = (process.env.ADMIN_IDS)
? process.env.ADMIN_IDS.split(',').map((z) => parseInt(z, 10)) : [];
export const CORS_HOSTS = (process.env.CORS_HOSTS)
? process.env.CORS_HOSTS.split(',') : [];
export const auth = {
// https://developers.facebook.com/
facebook: {

View File

@ -6,6 +6,7 @@ import Sequelize from 'sequelize';
import sequelize from '../data/sql/sequelize';
import RegUser from '../data/sql/RegUser';
import { saveDailyTop, loadDailyTop } from '../data/redis/PrevDayTop';
import socketEvents from '../socket/socketEvents';
import logger from './logger';
import { MINUTE } from './constants';
@ -33,15 +34,17 @@ class Ranks {
async updateRanking() {
logger.info('Update pixel rankings');
// recalculate ranking column
await sequelize.query(
// eslint-disable-next-line max-len
'SET @r=0; UPDATE Users SET ranking= @r:= (@r + 1) ORDER BY totalPixels DESC;',
);
await sequelize.query(
// eslint-disable-next-line max-len
'SET @r=0; UPDATE Users SET dailyRanking= @r:= (@r + 1) ORDER BY dailyTotalPixels DESC;',
);
if (socketEvents.amIImportant()) {
// recalculate ranking column
await sequelize.query(
// eslint-disable-next-line max-len
'SET @r=0; UPDATE Users SET ranking= @r:= (@r + 1) ORDER BY totalPixels DESC;',
);
await sequelize.query(
// eslint-disable-next-line max-len
'SET @r=0; UPDATE Users SET dailyRanking= @r:= (@r + 1) ORDER BY dailyTotalPixels DESC;',
);
}
// populate dictionaries
const ranking = await RegUser.findAll({
attributes: [
@ -92,6 +95,9 @@ class Ranks {
}
async resetDailyRanking() {
if (!socketEvents.amIImportant()) {
return;
}
this.prevTop = await saveDailyTop(this.ranks.dailyRanking);
logger.info('Resetting Daily Ranking');
await RegUser.update({ dailyTotalPixels: 0 }, { where: {} });

View File

@ -5,26 +5,36 @@ import expressSession from 'express-session';
import RedisStore from '../utils/connectRedis';
import client from '../data/redis/client';
import { getHostFromRequest } from '../utils/ip';
import { HOUR, COOKIE_SESSION_NAME } from './constants';
import { SESSION_SECRET } from './config';
export const store = new RedisStore({ client });
const session = expressSession({
name: COOKIE_SESSION_NAME,
store,
secret: SESSION_SECRET,
// The best way to know is to check with your store if it implements the touch method. If it does, then you can safely set resave: false
resave: false,
saveUninitialized: false,
cookie: {
path: '/',
httpOnly: true,
secure: false,
// not setting maxAge or expire makes it a non-persisting cookies
maxAge: 30 * 24 * HOUR,
},
});
/*
* we cache created session middlewares per domain
*/
const middlewareCache = {};
export default session;
export default (req, res, next) => {
const domain = getHostFromRequest(req, false, true);
let session = middlewareCache[domain];
if (!session) {
session = expressSession({
name: COOKIE_SESSION_NAME,
store,
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
domain,
httpOnly: true,
secure: false,
maxAge: 30 * 24 * HOUR,
},
});
middlewareCache[domain] = session;
}
return session(req, res, next);
};

View File

@ -8,7 +8,7 @@ import { Worker } from 'worker_threads';
import logger from './logger';
import canvases from './canvases';
import RedisCanvas from '../data/redis/RedisCanvas';
import socketEvents from '../socket/socketEvents';
import { TILE_FOLDER } from './config';
import {
@ -181,17 +181,16 @@ class CanvasUpdater {
}
}
export function registerChunkChange(canvasId, chunk) {
socketEvents.on('chunkUpdate', (canvasId, chunk) => {
if (CanvasUpdaters[canvasId]) {
CanvasUpdaters[canvasId].registerChunkChange(chunk);
}
}
RedisCanvas.setChunkChangeCallback(registerChunkChange);
});
/*
* starting update loops for canvases
*/
export async function startAllCanvasLoops() {
export default function startAllCanvasLoops() {
if (!fs.existsSync(`${TILE_FOLDER}`)) fs.mkdirSync(`${TILE_FOLDER}`);
const ids = Object.keys(canvases);
for (let i = 0; i < ids.length; i += 1) {

View File

@ -4,24 +4,13 @@
import { commandOptions } from 'redis';
import { getChunkOfPixel, getOffsetOfPixel } from '../../core/utils';
import socketEvents from '../../socket/socketEvents';
import client from './client';
const UINT_SIZE = 'u8';
class RedisCanvas {
// array of callback functions that gets informed about chunk changes
static registerChunkChange = [];
static setChunkChangeCallback(cb) {
RedisCanvas.registerChunkChange.push(cb);
}
static execChunkChangeCallback(canvasId, cell) {
for (let i = 0; i < RedisCanvas.registerChunkChange.length; i += 1) {
RedisCanvas.registerChunkChange[i](canvasId, cell);
}
}
/*
* Get chunk from redis
* canvasId integer id of canvas
@ -56,14 +45,14 @@ class RedisCanvas {
static async setChunk(i, j, chunk, canvasId) {
const key = `ch:${canvasId}:${i}:${j}`;
await client.set(key, Buffer.from(chunk.buffer));
RedisCanvas.execChunkChangeCallback(canvasId, [i, j]);
socketEvents.broadcastChunkUpdate(canvasId, [i, j]);
return true;
}
static async delChunk(i, j, canvasId) {
const key = `ch:${canvasId}:${i}:${j}`;
await client.del(key);
RedisCanvas.execChunkChangeCallback(canvasId, [i, j]);
socketEvents.broadcastChunkUpdate(canvasId, [i, j]);
return true;
}
@ -97,8 +86,6 @@ class RedisCanvas {
String(color),
],
);
RedisCanvas.execChunkChangeCallback(canvasId, [i, j]);
}
static flushPixels() {

View File

@ -4,13 +4,14 @@
*/
import fs from 'fs';
import { createClient, defineScript } from 'redis';
import { isMainThread } from 'worker_threads';
import { REDIS_URL } from '../../core/config';
import { REDIS_URL, SHARD_NAME } from '../../core/config';
const scripts = {
placePxl: defineScript({
NUMBER_OF_KEYS: 5,
SCRIPT: fs.readFileSync('./workers/placePixel.lua'),
SCRIPT: fs.readFileSync('./workers/lua/placePixel.lua'),
transformArguments(...args) {
return args.map((a) => ((typeof a === 'string') ? a : a.toString()));
},
@ -30,10 +31,24 @@ const client = createClient(REDIS_URL.startsWith('redis://')
},
);
/*
* for sending messages via cluster
*/
export const pubsub = {
subscriber: null,
publisher: null,
};
export const connect = async () => {
// eslint-disable-next-line no-console
console.log(`Connecting to redis server at ${REDIS_URL}`);
await client.connect();
if (SHARD_NAME && isMainThread) {
const subscriber = client.duplicate();
await subscriber.connect();
pubsub.publisher = client;
pubsub.subscriber = subscriber;
}
};
export default client;

View File

@ -0,0 +1,54 @@
/*
*
* data saving for hourly events
*
*/
import { randomUUID } from 'crypto';
import client from './client';
export const PREFIX = 'mail';
const EXPIRE_TIME = 3600;
/*
* generate and set mail code
* @param email
* @return code
*/
export function setCode(email) {
const code = randomUUID();
const key = `${PREFIX}:${email}`;
client.set(key, code, {
EX: EXPIRE_TIME,
});
return code;
}
/*
* check if email code is correct
* @param email
* @param code
*/
export async function checkCode(email, code) {
const key = `${PREFIX}:${email}`;
const storedCode = await client.get(key);
if (!storedCode || code !== storedCode) {
return false;
}
client.del(key);
return true;
}
/*
* check if code exists
* @param email
* @return null if doesn't, age in seconds if exists
*/
export async function codeExists(email) {
const key = `${PREFIX}:${email}`;
const ttl = await client.ttl(key);
if (!ttl) {
return null;
}
return EXPIRE_TIME - ttl;
}

View File

@ -2,7 +2,7 @@
* request password change
*/
import mailProvider from '../../../core/mail';
import mailProvider from '../../../core/MailProvider';
import { validatePassword, validateEMail } from '../../../utils/validation';
import { getHostFromRequest } from '../../../utils/ip';

View File

@ -3,7 +3,7 @@
*/
import socketEvents from '../../../socket/SocketEvents';
import socketEvents from '../../../socket/socketEvents';
import { RegUser } from '../../../data/sql';
import { validateName } from '../../../utils/validation';

View File

@ -2,6 +2,7 @@
* request password change
*/
import socketEvents from '../../../socket/socketEvents';
import { RegUser } from '../../../data/sql';
import { validatePassword } from '../../../utils/validation';
import { compareToHash } from '../../../utils/hash';
@ -35,7 +36,7 @@ export default async (req, res) => {
});
return;
}
const { id } = user;
const { id, name } = user;
const currentPassword = user.regUser.password;
if (!currentPassword || !compareToHash(password, currentPassword)) {
@ -58,6 +59,8 @@ export default async (req, res) => {
RegUser.destroy({ where: { id } });
socketEvents.reloadUser(name);
res.status(200);
res.json({
success: true,

View File

@ -2,7 +2,7 @@ import Sequelize from 'sequelize';
import logger from '../../../core/logger';
import { RegUser } from '../../../data/sql';
import mailProvider from '../../../core/mail';
import mailProvider from '../../../core/MailProvider';
import getMe from '../../../core/me';
import { getIPFromRequest, getHostFromRequest } from '../../../utils/ip';
import {

View File

@ -2,7 +2,7 @@
* request resend of verification mail
*/
import mailProvider from '../../../core/mail';
import mailProvider from '../../../core/MailProvider';
import { getHostFromRequest } from '../../../utils/ip';
export default async (req, res) => {
@ -26,7 +26,7 @@ export default async (req, res) => {
const host = getHostFromRequest(req);
const error = mailProvider.sendVerifyMail(email, name, host, lang);
const error = await mailProvider.sendVerifyMail(email, name, host, lang);
if (error) {
res.status(400);
res.json({

View File

@ -3,7 +3,7 @@
*/
import mailProvider from '../../../core/mail';
import mailProvider from '../../../core/MailProvider';
import { validateEMail } from '../../../utils/validation';
import { getHostFromRequest } from '../../../utils/ip';

View File

@ -2,31 +2,36 @@
* verify mail address
*/
import socketEvents from '../../../socket/SocketEvents';
import socketEvents from '../../../socket/socketEvents';
import getHtml from '../../../ssr/RedirectionPage';
import { getHostFromRequest } from '../../../utils/ip';
import mailProvider from '../../../core/mail';
import { MailProvider } from '../../../core/MailProvider';
import { validateEMail } from '../../../utils/validation';
export default async (req, res) => {
const { token } = req.query;
const { email, token } = req.query;
const { lang } = req;
const { t } = req.ttag;
const name = await mailProvider.verify(token);
const host = getHostFromRequest(req);
if (name) {
// notify websoecket to reconnect user
// thats a bit counter productive because it directly links to the websocket
socketEvents.reloadUser(name);
// ---
const index = getHtml(
t`Mail verification`,
t`You are now verified :)`,
host, lang,
);
res.status(200).send(index);
} else {
// eslint-disable-next-line max-len
const index = getHtml(t`Mail verification`, t`Your mail verification code is invalid or already expired :(, please request a new one.`, host, lang);
res.status(400).send(index);
const error = validateEMail(email);
if (!error) {
const name = await MailProvider.verify(email, token);
if (name) {
// notify websoecket to reconnect user
// thats a bit counter productive because it directly links to the websocket
socketEvents.reloadUser(name);
// ---
const index = getHtml(
t`Mail verification`,
t`You are now verified :)`,
host, lang,
);
res.status(200).send(index);
return;
}
}
// eslint-disable-next-line max-len
const index = getHtml(t`Mail verification`, t`Your mail verification code is invalid or already expired :(, please request a new one.`, host, lang);
res.status(400).send(index);
};

View File

@ -5,7 +5,7 @@
*/
import logger from '../../core/logger';
import socketEvents from '../../socket/SocketEvents';
import socketEvents from '../../socket/socketEvents';
import { RegUser, UserBlock, Channel } from '../../data/sql';
async function block(req, res) {

View File

@ -4,7 +4,7 @@
*
*/
import logger from '../../core/logger';
import socketEvents from '../../socket/SocketEvents';
import socketEvents from '../../socket/socketEvents';
async function blockdm(req, res) {
const { block } = req.body;

View File

@ -7,11 +7,6 @@ import chatProvider from '../../core/ChatProvider';
async function chatHistory(req, res) {
let { cid, limit } = req.query;
res.set({
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
});
if (!cid || !limit) {
res.status(400);
@ -42,19 +37,10 @@ async function chatHistory(req, res) {
return;
}
// try {
const history = await chatProvider.getHistory(cid, limit);
res.json({
history,
});
/*
} catch {
res.status(500);
res.json({
errors: ['Can not fetch messages'],
});
}
*/
}
export default chatHistory;

View File

@ -17,10 +17,19 @@ import blockdm from './blockdm';
import modtools from './modtools';
import baninfo from './baninfo';
import getiid from './getiid';
import shards from './shards';
const router = express.Router();
// set cache-control
router.use((req, res, next) => {
res.set({
'Cache-Control': 'no-cache, no-store, must-revalidate',
Expires: '0',
});
next();
});
router.use(express.json());
// eslint-disable-next-line no-unused-vars
@ -35,6 +44,7 @@ router.use((err, req, res, next) => {
router.post('/captcha', captcha);
router.get('/baninfo', baninfo);
router.get('/getiid', getiid);
router.get('/shards', shards);
/*
* get user session

View File

@ -5,7 +5,7 @@
*/
import logger from '../../core/logger';
import socketEvents from '../../socket/SocketEvents';
import socketEvents from '../../socket/socketEvents';
async function leaveChan(req, res) {
const channelId = parseInt(req.body.channelId, 10);

View File

@ -24,10 +24,6 @@ export default async (req, res, next) => {
}
// https://stackoverflow.com/questions/49547/how-to-control-web-page-caching-across-all-browsers
res.set({
'Cache-Control': 'no-cache, no-store, must-revalidate',
Expires: '0',
});
res.json(userdata);
} catch (error) {
next(error);

20
src/routes/api/shards.js Normal file
View File

@ -0,0 +1,20 @@
/*
* print information for shards
*/
import socketEvents from '../../socket/socketEvents';
async function shards(req, res, next) {
try {
if (!socketEvents.isCluster) {
res.status(400).json({
errors: ['Not running as cluster'],
});
return;
}
res.status(200).json(socketEvents.shardOnlineCounters);
} catch (err) {
next(err);
}
}
export default shards;

View File

@ -7,11 +7,11 @@
import etag from 'etag';
import RedisCanvas from '../data/redis/RedisCanvas';
import logger from '../core/logger';
import socketEvents from '../socket/socketEvents';
const chunkEtags = new Map();
RedisCanvas.setChunkChangeCallback((canvasId, cell) => {
const [x, y] = cell;
chunkEtags.delete(`${canvasId}:${x}:${y}`);
socketEvents.on('chunkUpdate', (canvasId, [i, j]) => {
chunkEtags.delete(`${canvasId}:${i}:${j}`);
});
/*
@ -27,6 +27,9 @@ export default async (req, res, next) => {
const x = parseInt(paramX, 10);
const y = parseInt(paramY, 10);
try {
res.set({
'Access-Control-allow-origin': '*',
});
// botters where using cachebreakers to update via chunk API
// lets not allow that for now
if (Object.keys(req.query).length !== 0) {

View File

@ -18,6 +18,7 @@ import api from './api';
import { assets } from '../core/assets';
import { expressTTag } from '../core/ttag';
import corsMiddleware from '../utils/corsMiddleware';
import generateGlobePage from '../ssr/Globe';
import generatePopUpPage from '../ssr/PopUp';
import generateMainPage from '../ssr/Main';
@ -29,37 +30,18 @@ import { GUILDED_INVITE } from '../core/config';
const router = express.Router();
/*
* void info
* Serving Chunks
*/
router.get('/void', voidl);
/*
* ranking of pixels placed
* daily and total
*/
router.get('/ranking', ranking);
/*
* give: date per query
* returns: array of HHMM backups available
*/
router.get('/history', history);
router.get(
'/chunks/:c([0-9]+)/:x([0-9]+)/:y([0-9]+)(/)?:z([0-9]+)?.bmp',
chunks,
);
/*
* zoomed tiles
*/
router.use('/tiles', tiles);
/*
* adminapi