Merge branch 'captcha'

This commit is contained in:
HF 2021-03-18 01:47:23 +01:00
commit e237d74481
60 changed files with 5345 additions and 2537 deletions

View File

@ -30,6 +30,7 @@
"react/jsx-one-expression-per-line": "off",
"react/jsx-closing-tag-location":"off",
"jsx-a11y/click-events-have-key-events":"off",
"jsx-a11y/no-static-element-interactions":"off",
"no-continue": "off",
"no-multiple-empty-lines": "off",
"lines-between-class-members":["warn", "always",{"exceptAfterSingleLine": true}]

View File

@ -69,8 +69,9 @@ Configuration takes place in the environment variables that are defined in ecosy
| Variable | Description | Example |
|----------------|:-------------------------|------------------------:|
| PORT | Port | 80 |
| REDIS_URL | URL:PORT of redis server | "redis://localhost:6379" |
| PORT | Own Port | 80 |
| HOST | Own Host | "localhost" |
| REDIS_URL | URL:PORT of redis server | "redis://localhost:6379"|
| MYSQL_HOST | MySql Host | "localhost" |
| MYSQL_USER | MySql User | "user" |
| MYSQL_PW | MySql Password | "password" |
@ -78,24 +79,22 @@ Configuration takes place in the environment variables that are defined in ecosy
#### Optional Configuration
| Variable | Description | Example |
|-------------------|:--------------------------------------|--------------------|
| ASSET_SERVER | URL for assets | "http://localhost" |
| USE_PROXYCHECK | Check users for Proxies | 0 |
| APISOCKET_KEY | Key for API Socket for SpecialAccess™ | "SDfasife3" |
| ADMIN_IDS | Ids of users with Admin rights | "1,12,3" |
| CAPTCHA_METHOD | 0: none, 1: reCaptcha, 2: hCaptcha | 2 |
| CAPTCHA_SECRET | re/hCaptcha secret key | "asdieewff" |
| CAPTCHA_SITEKEY | re/hCaptcha site key | "23ksdfssd" |
| CAPTCHA_TIME | time in minutes between captchas | 30 |
| SESSION_SECRET | random sting for express sessions | "ayylmao" |
| LOG_MYSQL | if sql queries should get logged | 0 |
| USE_XREALIP | see cloudflare section | 1 |
| BACKUP_URL | url of backup server (see Backup) | "http://localhost" |
| BACKUP_DIR | mounted directory of backup server | "/mnt/backup/" |
| GMAIL_USER | gmail username if used for mails | "ppfun@gmail.com" |
| GMAIL_PW | gmail password if used for mails | "lolrofls" |
| HOURLY_EVENT | run hourly void event on main canvas | 1 |
| Variable | Description | Example |
|-------------------|:--------------------------------------|-------------------------|
| ASSET_SERVER | URL for assets | "http://localhost" |
| USE_PROXYCHECK | Check users for Proxies | 0 |
| APISOCKET_KEY | Key for API Socket for SpecialAccess™ | "SDfasife3" |
| ADMIN_IDS | Ids of users with Admin rights | "1,12,3" |
| CAPTCHA_URL | URL where captcha is served | "http://localhost:8080" |
| CAPTCHA_TIME | time in minutes between captchas | 30 |
| SESSION_SECRET | random sting for express sessions | "ayylmao" |
| LOG_MYSQL | if sql queries should get logged | 0 |
| USE_XREALIP | see ngins / CDN section | 1 |
| BACKUP_URL | url of backup server (see Backup) | "http://localhost" |
| BACKUP_DIR | mounted directory of backup server | "/mnt/backup/" |
| GMAIL_USER | gmail username if used for mails | "ppfun@gmail.com" |
| GMAIL_PW | gmail password if used for mails | "lolrofls" |
| HOURLY_EVENT | run hourly void event on main canvas | 1 |
Notes:
@ -108,7 +107,7 @@ Notes:
| Variable | Description |
|-----------------------|:-------------------------|
| DISCORD_INVITE | Invite to discord server |
| GUILDED_INVITE | Invite to guilded server |
| DISCORD_CLIENT_ID | All |
| DISCORD_CLIENT_SECRET | those |
| GOOGLE_CLIENT_ID | values |

View File

@ -0,0 +1,9 @@
apps:
- script : ./captchaserver.js
name : 'captchas'
node_args: --nouse-idle-notification --expose-gc
env:
PORT: 8080
HOST: "localhost"
REDIS_URL: 'redis://localhost:6379'
CAPTCHA_TIMEOUT: 120

View File

@ -3,9 +3,9 @@ apps:
name : 'web'
node_args: --nouse-idle-notification --expose-gc
env:
HOSTURL: "http://localhost"
ASSET_SERVER: "http://localhost"
PORT: 80
HOST: "localhost"
ASSET_SERVER: "http://localhost"
REDIS_URL: 'redis://localhost:6379'
MYSQL_HOST: "localhost"
MYSQL_USER: "pixelplanet"

View File

@ -42,8 +42,10 @@ do
cd "$PFOLDER"
pm2 stop web
pm2 stop backup
pm2 stop captchas
pm2 start ecosystem.yml
pm2 start ecosystem-backup.yml
pm2 start ecosystem-captchas.yml
curl -H "Content-Type: application/json" --data-binary '{ "username": "PixelPlanet Server", "avatar_url": "https://pixelplanet.fun/favicon.ico", "content": "...Done", "embeds": [{"title": "New Commits", "url": "https://pixelplanet.fun", "description": "'"$COMMITS"'", "color": 15258703}] }' "$PWEBHOOK"
else
echo "---UPDATING REPO ON DEV SERVER---"

View File

@ -228,8 +228,8 @@ msgid ""
"already set pixels."
msgstr ""
"Nosso canvas principal é um mapa mundi enorme, que você pode colocar píxeis "
"onde quiser, mas você tera que esperar um tempo entre píxeis. Você pode ver "
"o tempo de espera entre píxeis e outros requerimentos no menu de Seleção de "
"onde quiser, mas você tera que esperar um tempo entre píxeis. Você pode ver o "
"tempo de espera entre píxeis e outros requerimentos no menu de Seleção de "
"Canvas (o botão com o formato de globo no topo). Alguns canvas tem um tempo "
"de espera diferente para trocar píxeis que foram colocados por outro usuário "
"do que um píxeis que ainda não foram pintado. Por exemplo: 4s/7s significa 4 "
@ -265,8 +265,8 @@ msgid ""
"The bare map data that we use, together with converted OpenStreetMap tiles "
"for orientation, can be downloaded from mega.nz here: "
msgstr ""
"O mapa original que usamos, junto com os pedaços convertidos do "
"OpenStreetMap para orientação, podem ser baixados do mega.nz aqui: "
"O mapa original que usamos, junto com os pedaços convertidos do OpenStreetMap "
"para orientação, podem ser baixados do mega.nz aqui: "
#: src/components/HelpModal.jsx:59
msgid "Detected as Proxy?"
@ -365,8 +365,8 @@ msgid ""
"Click ${ mouseSymbol } middle mouse button or ${ touchSymbol } long-tap to "
"select current hovering color"
msgstr ""
"Clique ${ mouseSymbol } com botão do meio do mouse ou ${ touchSymbol } "
"toque e segure para selecionar a cor que o mouse/seu dedo está em cima"
"Clique ${ mouseSymbol } com botão do meio do mouse ou ${ touchSymbol } toque "
"e segure para selecionar a cor que o mouse/seu dedo está em cima"
#: src/components/HelpModal.jsx:84
msgid "Press ${ bindE } and ${ bindC } to fly up and down"
@ -469,8 +469,8 @@ msgid ""
"Zoom in instead of placing a pixel when you tap the canvas and your zoom is "
"small."
msgstr ""
"Dar zoom ao invés de colocar um pixel quando você aperta no canvas e seu "
"zoom é baixo."
"Dar zoom ao invés de colocar um pixel quando você aperta no canvas e seu zoom "
"é baixo."
#: src/components/SettingsModal.jsx:158
msgid "Compact Palette"
@ -478,8 +478,7 @@ msgstr "Paleta Compacta"
#: src/components/SettingsModal.jsx:160
msgid "Display Palette in a compact form that takes less screen space."
msgstr ""
"Mostrar a paleta em um formato compacto que ocupa menos espaço da tela."
msgstr "Mostrar a paleta em um formato compacto que ocupa menos espaço da tela."
#: src/components/SettingsModal.jsx:165
msgid "Potato Mode"
@ -517,8 +516,8 @@ msgstr "Como pixelplanet deve parecer."
msgid "Register new account here"
msgstr "Registre uma conta nova aqui"
#: src/components/ForgotPasswordModal.jsx:20
#: src/components/RegisterModal.jsx:21 src/components/UserAreaModal.jsx:130
#: src/components/ForgotPasswordModal.jsx:20 src/components/RegisterModal.jsx:21
#: src/components/UserAreaModal.jsx:130
msgid "Consider joining us on Guilded:"
msgstr "Considere entrar no nosso Guilded:"
@ -576,16 +575,16 @@ msgstr "Carregando..."
#: src/components/ArchiveModal.jsx:20
msgid ""
"While we tend to not delete canvases, some canvases are started for fun or "
"as a request by users who currently like a meme. Those canvases can get "
"boring after a while and after weeks of no major change and if they really "
"aren't worth being kept active, we decide to remove them."
"While we tend to not delete canvases, some canvases are started for fun or as "
"a request by users who currently like a meme. Those canvases can get boring "
"after a while and after weeks of no major change and if they really aren't "
"worth being kept active, we decide to remove them."
msgstr ""
"Enquanto a gente não costuma deletar os canvas, alguns canvas são feitos "
"apenas como piada ou alguns usuários pedem porque gostam de um meme. Esses "
"canvas depois de um tempo podem ficar chatos e entediantes e se não forem "
"bastante modificados depois de semanas e não valerem a pena se manter "
"ativos, nós decidimos removê-los."
"bastante modificados depois de semanas e não valerem a pena se manter ativos, "
"nós decidimos removê-los."
#: src/components/ArchiveModal.jsx:22
msgid ""
@ -602,9 +601,9 @@ msgstr "Canvas do Compasso Político"
#: src/components/ArchiveModal.jsx:31
msgid ""
"This canvas got requested during a time of political conflicts on the main "
"Earth canvas. It was a 1024x1024 representation of the political compass "
"with a 5s cooldown and 60s stacking. It got launched on May 11th and "
"remained active for months till it got shut down on November 30th."
"Earth canvas. It was a 1024x1024 representation of the political compass with "
"a 5s cooldown and 60s stacking. It got launched on May 11th and remained "
"active for months till it got shut down on November 30th."
msgstr ""
"Este canvas foi pedido durante um tempo de muitos conflitos políticos no "
"canvas principal, a Terra. Era uma representação com 1024x1024 píxeis do "
@ -618,8 +617,8 @@ msgid ""
"screenshot from the timelapse results in a perfect 1:1 representation of how "
"the canvas was at that time."
msgstr ""
"Nos decidimos arquivá-lo como uma timelapse em um webm codificado sem "
"perdas. Tirando uma captura de tela da timelapse resulta em uma perfeita "
"Nos decidimos arquivá-lo como uma timelapse em um webm codificado sem perdas. "
"Tirando uma captura de tela da timelapse resulta em uma perfeita "
"representação 1:1 de como o canvas era naquele tempo."
#: src/components/ArchiveModal.jsx:50
@ -666,7 +665,7 @@ msgstr "Nome ou Email"
#: src/components/ChangeMail.jsx:112 src/components/DeleteAccount.jsx:89
#: src/components/LogInForm.jsx:111 src/components/SignUpForm.jsx:140
msgid "Password"
msgstr "SeNHA"
msgstr "Senha"
#: src/components/LogInForm.jsx:115
msgid "LogIn"

View File

@ -73,11 +73,11 @@ msgstr ""
msgid "A 3D globe of our whole map"
msgstr ""
#: src/ssr-components/Main.jsx:72
#: src/ssr-components/Main.jsx:73
msgid "PixelPlanet.fun"
msgstr ""
#: src/ssr-components/Main.jsx:74
#: src/ssr-components/Main.jsx:75
msgid "Place color pixels on an map styled canvas with other players online"
msgstr ""
@ -270,39 +270,28 @@ msgstr ""
msgid "Password must be shorter than 60 characters."
msgstr ""
#: src/utils/validation.js:74
msgid "Could not connect to server, please try again later :("
#: src/routes/api/captcha.js:22
msgid "No captcha text given"
msgstr ""
#: src/utils/validation.js:80
msgid "I think we experienced some error :("
#: src/routes/api/captcha.js:36
msgid "You took too long, try again."
msgstr ""
#: src/routes/api/auth/change_mail.js:41
#: src/routes/api/auth/change_passwd.js:37
#: src/routes/api/auth/delete_account.js:38
msgid "You are not authenticated."
#: src/routes/api/captcha.js:42
msgid "You failed your captcha"
msgstr ""
#: src/routes/api/auth/change_mail.js:50
#: src/routes/api/auth/change_passwd.js:46
#: src/routes/api/auth/delete_account.js:48
msgid "Incorrect password!"
#: src/routes/api/captcha.js:48
msgid "Unknown Captcha Error"
msgstr ""
#: src/routes/api/auth/verify.js:25
#: src/routes/api/auth/verify.js:32
msgid "Mail verification"
#: src/routes/api/captcha.js:55
msgid "Server error occured"
msgstr ""
#: src/routes/api/auth/verify.js:26
msgid "You are now verified :)"
msgstr ""
#: src/routes/api/auth/verify.js:32
msgid ""
"Your mail verification code is invalid or already expired :(, please "
"request a new one."
#: src/routes/api/auth/logout.js:13
msgid "You are not even logged in."
msgstr ""
#: src/routes/api/auth/register.js:31
@ -321,8 +310,31 @@ msgstr ""
msgid "Failed to establish session after register :("
msgstr ""
#: src/routes/api/auth/logout.js:13
msgid "You are not even logged in."
#: src/routes/api/auth/verify.js:25
#: src/routes/api/auth/verify.js:32
msgid "Mail verification"
msgstr ""
#: src/routes/api/auth/verify.js:26
msgid "You are now verified :)"
msgstr ""
#: src/routes/api/auth/verify.js:32
msgid ""
"Your mail verification code is invalid or already expired :(, please "
"request a new one."
msgstr ""
#: src/routes/api/auth/change_mail.js:41
#: src/routes/api/auth/change_passwd.js:37
#: src/routes/api/auth/delete_account.js:38
msgid "You are not authenticated."
msgstr ""
#: src/routes/api/auth/change_mail.js:50
#: src/routes/api/auth/change_passwd.js:46
#: src/routes/api/auth/delete_account.js:48
msgid "Incorrect password!"
msgstr ""
#: src/ssr-components/RedirectionPage.jsx:20

File diff suppressed because it is too large Load Diff

4670
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"main": "server.js",
"scripts": {
"build": "babel-node scripts/run prebuild && npm run webpack",
"build-en": "babel-node scripts/run prebuild && npm run extract",
"clean": "babel-node scripts/run clean",
"webpack": "webpack --config ./webpack.config.web.babel.js && parallel-webpack --config ./webpack.config.client.babel.js",
"extract": "webpack --env extract --config ./webpack.config.web.babel.js && webpack --env extract --config ./webpack.config.client.babel.js",
@ -27,14 +28,14 @@
"not IE_Mob 11"
],
"dependencies": {
"bcrypt": "^5.0.0",
"bcrypt": "^5.0.1",
"bluebird": "^3.5.0",
"body-parser": "^1.17.2",
"bufferutil": "^4.0.3",
"compression": "^1.7.3",
"connect-redis": "^5.0.0",
"connect-redis": "^5.1.0",
"cookie": "^0.4.1",
"core-js": "^3.8.3",
"core-js": "^3.9.1",
"cors": "^2.8.4",
"etag": "^1.8.1",
"express": "^4.15.3",
@ -50,7 +51,7 @@
"morgan": "^1.10.0",
"multer": "^1.4.1",
"mysql2": "^2.2.5",
"nodemailer": "^6.4.17",
"nodemailer": "^6.5.0",
"passport": "^0.4.0",
"passport-discord": "^0.1.4",
"passport-facebook": "^3.0.0",
@ -58,10 +59,10 @@
"passport-json": "^1.2.0",
"passport-reddit": "^0.2.4",
"passport-vkontakte": "^0.5.0",
"ppfun-captcha": "^1.6.4",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-icons": "^4.1.0",
"react-modal": "^3.12.1",
"react-icons": "^4.2.0",
"react-redux": "^7.2.1",
"react-responsive": "^8.2.0",
"react-stay-scrolled": "^7.3.0",
@ -72,59 +73,58 @@
"redux-logger": "^3.0.6",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.2.0",
"sequelize": "^6.5.0",
"sharp": "^0.27.1",
"sequelize": "^6.5.1",
"sharp": "^0.27.2",
"startaudiocontext": "^1.2.1",
"sweetalert2": "^10.14.0",
"three": "^0.125.2",
"three-trackballcontrols": "^0.9.0",
"ttag": "^1.7.24",
"ttag-po-loader": "0.0.2",
"url-search-params-polyfill": "^8.1.0",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.0",
"ws": "^7.4.2"
"winston-daily-rotate-file": "^4.5.1",
"ws": "^7.4.4"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.12.12",
"@babel/plugin-proposal-do-expressions": "^7.10.4",
"@babel/plugin-proposal-export-default-from": "^7.10.4",
"@babel/plugin-proposal-export-namespace-from": "^7.10.4",
"@babel/plugin-proposal-function-bind": "^7.11.5",
"@babel/plugin-proposal-function-sent": "^7.10.4",
"@babel/plugin-proposal-json-strings": "^7.10.4",
"@babel/plugin-proposal-logical-assignment-operators": "^7.11.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
"@babel/plugin-proposal-optional-chaining": "^7.12.7",
"@babel/plugin-proposal-pipeline-operator": "^7.10.5",
"@babel/plugin-proposal-throw-expressions": "^7.10.4",
"@babel/core": "^7.13.10",
"@babel/node": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.5",
"@babel/plugin-proposal-do-expressions": "^7.12.13",
"@babel/plugin-proposal-export-default-from": "^7.12.13",
"@babel/plugin-proposal-export-namespace-from": "^7.12.13",
"@babel/plugin-proposal-function-bind": "^7.12.13",
"@babel/plugin-proposal-function-sent": "^7.12.13",
"@babel/plugin-proposal-json-strings": "^7.13.8",
"@babel/plugin-proposal-logical-assignment-operators": "^7.13.8",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
"@babel/plugin-proposal-numeric-separator": "^7.12.13",
"@babel/plugin-proposal-object-rest-spread": "^7.13.8",
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
"@babel/plugin-proposal-pipeline-operator": "^7.12.13",
"@babel/plugin-proposal-throw-expressions": "^7.12.13",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-transform-flow-strip-types": "^7.12.10",
"@babel/plugin-transform-react-constant-elements": "^7.10.4",
"@babel/plugin-transform-react-inline-elements": "^7.10.4",
"@babel/plugin-transform-flow-strip-types": "^7.13.0",
"@babel/plugin-transform-react-constant-elements": "^7.13.10",
"@babel/plugin-transform-react-inline-elements": "^7.12.13",
"@babel/polyfill": "^7.11.5",
"@babel/preset-env": "^7.12.11",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/preset-env": "^7.13.10",
"@babel/preset-flow": "^7.12.13",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.13.0",
"assets-webpack-plugin": "^7.0.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"babel-plugin-transform-react-pure-class-to-function": "^1.0.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-plugin-ttag": "^1.7.30",
"clean-css": "^5.0.1",
"css-loader": "^5.0.1",
"eslint": "^7.19.0",
"clean-css": "^5.1.1",
"css-loader": "^5.1.3",
"eslint": "^7.22.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-flowtype": "^5.4.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.22.0",
@ -140,9 +140,9 @@
"rimraf": "^3.0.2",
"style-loader": "^2.0.0",
"ttag-cli": "^1.9.1",
"webpack": "^5.19.0",
"webpack": "^5.26.3",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.4.0",
"webpack-cli": "^4.5.0",
"webpack-dev-middleware": "^4.1.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-node-externals": "^2.5.2",

View File

@ -92,6 +92,10 @@ async function copy() {
`${deploydir}/example-ecosystem-backup.yml`,
`${builddir}/ecosystem-backup.example.yml`,
),
copyFile(
`${deploydir}/example-ecosystem-captchas.yml`,
`${builddir}/ecosystem-captchas.example.yml`,
),
]);
}

View File

@ -1,11 +1,11 @@
/*
* Collect api fetch commands for actions here
* (chunk and tiles requests in ui/ChunkLoader*.js)
* (user settings requests in their components)
*
* @flow
*/
import { t } from 'ttag';
/*
* Adds customizeable timeout to fetch
@ -27,123 +27,226 @@ async function fetchWithTimeout(resource, options) {
}
/*
* block / unblock user
* userId id of user to block
* block true if block, false if unblock
* return error string or null if successful
* Parse response from API
* @param response
* @return Object of response
*/
export async function requestBlock(userId: number, block: boolean) {
const response = await fetchWithTimeout('api/block', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
block,
}),
});
async function parseAPIresponse(response) {
const { status: code } = response;
if (code === 429) {
let error = t`You made too many requests`;
const retryAfter = response.headers.get('Retry-After');
if (!Number.isNaN(Number(retryAfter))) {
const ti = Math.floor(retryAfter / 60);
error += `, ${t`try again after ${ti}min`}`;
}
return {
errors: [error],
};
}
try {
const res = await response.json();
if (res.errors) {
return res.errors[0];
}
if (response.ok && res.status === 'ok') {
return null;
}
return 'Unknown Error';
} catch {
return 'Connection Error';
return await response.json();
} catch (e) {
return {
errors: [t`Connection error ${code} :(`],
};
}
}
/*
* Make API POST Request
* @param url URL of post api endpoint
* @param body Body of request
* @return Object with response or error Array
*/
async function makeAPIPOSTRequest(url, body) {
try {
const response = await fetchWithTimeout(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
return parseAPIresponse(response);
} catch (e) {
return {
errors: [t`Could not connect to server, please try again later :(`],
};
}
}
/*
* Make API GET Request
* @param url URL of get api endpoint
* @return Object with response or error Array
*/
async function makeAPIGETRequest(url) {
try {
const response = await fetchWithTimeout(url, {
credentials: 'include',
});
return parseAPIresponse(response);
} catch (e) {
return {
errors: [t`Could not connect to server, please try again later :(`],
};
}
}
/*
* block / unblock user
* @param userId id of user to block
* @param block true if block, false if unblock
* @return error string or null if successful
*/
export async function requestBlock(userId: number, block: boolean) {
const res = await makeAPIPOSTRequest(
'api/block',
{ userId, block },
);
if (res.errors) {
return res.errors[0];
}
if (res.status === 'ok') {
return null;
}
return t`Unknown Error`;
}
/*
* start new DM channel with user
* query Object with either userId: number or userName: string
* return channel Array on success, error string if not
* @param query Object with either userId: number or userName: string
* @return channel Array on success, error string if not
*/
export async function requestStartDm(query) {
const response = await fetchWithTimeout('api/startdm', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(query),
});
try {
const res = await response.json();
if (res.errors) {
return res.errors[0];
}
if (response.ok && res.channel) {
const { channel } = res;
return channel;
}
return 'Unknown Error';
} catch {
return 'Connection Error';
const res = await makeAPIPOSTRequest(
'api/startdm',
query,
);
if (res.errors) {
return res.errors[0];
}
if (res.channel) {
return res.channel;
}
return t`Unknown Error`;
}
/*
* set receiving of all DMs on/off
* block true if blocking all dms, false if unblocking
* return error string or null if successful
* @param block true if blocking all dms, false if unblocking
* @return error string or null if successful
*/
export async function requestBlockDm(block: boolean) {
const response = await fetchWithTimeout('api/blockdm', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ block }),
});
try {
const res = await response.json();
if (res.errors) {
return res.errors[0];
}
if (response.ok && res.status === 'ok') {
return null;
}
return 'Unknown Error';
} catch {
return 'Connection Error';
const res = await makeAPIPOSTRequest(
'api/blockdm',
{ block },
);
if (res.errors) {
return res.errors[0];
}
if (res.status === 'ok') {
return null;
}
return t`Unknown Error`;
}
/*
* leaving Chat Channel (i.e. DM channel)
* channelId 8nteger id of channel
* return error string or null if successful
* @param channelId 8nteger id of channel
* @return error string or null if successful
*/
export async function requestLeaveChan(channelId: boolean) {
const response = await fetchWithTimeout('api/leavechan', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ channelId }),
});
try {
const res = await response.json();
if (res.errors) {
return res.errors[0];
}
if (response.ok && res.status === 'ok') {
return null;
}
return 'Unknown Error';
} catch {
return 'Connection Error';
const res = await makeAPIPOSTRequest(
'api/leavechan',
{ channelId },
);
if (res.errors) {
return res.errors[0];
}
if (res.status === 'ok') {
return null;
}
return t`Unknown Error`;
}
export async function requestSolveCaptcha(text) {
const res = await makeAPIPOSTRequest(
'api/captcha',
{ text },
);
if (!res.errors && !res.success) {
return {
errors: [t`Server answered with gibberish :(`],
};
}
return res;
}
export function requestPasswordChange(newPassword, password) {
return makeAPIPOSTRequest(
'api/auth/change_passwd',
{ password, newPassword },
);
}
export async function requestResendVerify() {
return makeAPIGETRequest(
'./api/auth/resend_verify',
);
}
export function requestMcLink(accepted) {
return makeAPIPOSTRequest(
'api/auth/mclink',
{ accepted },
);
}
export function requestNameChange(name) {
return makeAPIPOSTRequest(
'api/auth/change_name',
{ name },
);
}
export function requestMailChange(email, password) {
return makeAPIPOSTRequest(
'api/auth/change_mail',
{ email, password },
);
}
export function requestLogin(nameoremail, password) {
return makeAPIPOSTRequest(
'api/auth/local',
{ nameoremail, password },
);
}
export function requestRegistration(name, email, password) {
return makeAPIPOSTRequest(
'api/auth/register',
{ name, email, password },
);
}
export function requestNewPassword(email) {
return makeAPIPOSTRequest(
'api/auth/restore_password',
{ email },
);
}
export function requestDeleteAccount(password) {
return makeAPIPOSTRequest(
'api/auth/delete_account',
{ password },
);
}

View File

@ -29,6 +29,12 @@ export function sweetAlert(
};
}
export function closeAlert(): Action {
return {
type: 'CLOSE_ALERT',
};
}
export function toggleChatBox(): Action {
return {
type: 'TOGGLE_CHAT_BOX',
@ -114,9 +120,9 @@ export function toggleOpenMenu(): Action {
};
}
export function setPlaceAllowed(requestingPixel: boolean): Action {
export function setRequestingPixel(requestingPixel: boolean): Action {
return {
type: 'SET_PLACE_ALLOWED',
type: 'SET_REQUESTING_PIXEL',
requestingPixel,
};
}

View File

@ -14,6 +14,7 @@ export type Action =
icon: string,
confirmButtonText: string,
}
| { type: 'CLOSE_ALERT' }
| { type: 'TOGGLE_GRID' }
| { type: 'TOGGLE_PIXEL_NOTIFY' }
| { type: 'TOGGLE_AUTO_ZOOM_IN' }
@ -28,7 +29,7 @@ export type Action =
| { type: 'SELECT_STYLE', style: string }
| { type: 'SET_NOTIFICATION', notification: string }
| { type: 'UNSET_NOTIFICATION' }
| { type: 'SET_PLACE_ALLOWED', requestingPixel: boolean }
| { type: 'SET_REQUESTING_PIXEL', requestingPixel: boolean }
| { type: 'SET_HOVER', hover: Cell }
| { type: 'UNSET_HOVER' }
| { type: 'SET_WAIT', wait: ?number }

45
src/captchaserver.js Normal file
View File

@ -0,0 +1,45 @@
/*
* serving captchas
*/
/* eslint-disable no-console */
import process from 'process';
import http from 'http';
import ppfunCaptcha from 'ppfun-captcha';
import { getIPFromRequest } from './utils/ip';
import { setCaptchaSolution } from './utils/captcha';
const PORT = process.env.PORT || 8080;
const HOST = process.env.HOST || 'localhost';
const server = http.createServer((req, res) => {
const captcha = ppfunCaptcha.create({
width: 500,
height: 300,
fontSize: 220,
stroke: 'black',
fill: 'none',
nodeDeviation: 3.0,
connectionPathDeviation: 3.0,
style: 'stroke-width: 4;',
background: '#EFEFEF',
});
const ip = getIPFromRequest(req);
setCaptchaSolution(captcha.text, ip);
console.log(`Serving ${captcha.text} to ${ip}`);
res.writeHead(200, {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'no-cache',
});
res.write(captcha.data);
res.end();
});
server.listen(PORT, HOST, () => {
console.log(`Captcha Server listening on port ${PORT}`);
});

74
src/components/Alert.jsx Normal file
View File

@ -0,0 +1,74 @@
/*
*
* @flow
*/
import React, { useState, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Captcha from './Captcha';
import { closeAlert } from '../actions';
const Alert = () => {
const [render, setRender] = useState(false);
const {
alertOpen,
alertType,
alertTitle,
alertMessage,
alertBtn,
} = useSelector((state) => state.alert);
const dispatch = useDispatch();
const close = useCallback(() => {
dispatch(closeAlert());
}, [dispatch]);
const onTransitionEnd = () => {
if (!alertOpen) setRender(false);
};
useEffect(() => {
window.setTimeout(() => {
if (alertOpen) setRender(true);
}, 10);
}, [alertOpen]);
return (
(render || alertOpen) && (
<div>
<div
className={(alertOpen && render)
? 'OverlayAlert show'
: 'OverlayAlert'}
onTransitionEnd={onTransitionEnd}
tabIndex={-1}
onClick={close}
/>
<div
className={(alertOpen && render) ? 'Alert show' : 'Alert'}
>
<h2>{alertTitle}</h2>
<p className="modaltext">
{alertMessage}
</p>
<p>
{(alertType === 'captcha')
? <Captcha close={close} />
: (
<button
type="button"
onClick={close}
>
{alertBtn}
</button>
)}
</p>
</div>
</div>
)
);
};
export default React.memo(Alert);

127
src/components/Captcha.jsx Normal file
View File

@ -0,0 +1,127 @@
/*
* Form to ask for captcha.
* If callback is provided, it sets the captcha text to it.
* If callback is not provided, it provides a button to send the
* captcha itself
* @flow
*/
import React, { useState } from 'react';
import { t } from 'ttag';
import { IoReloadCircleSharp } from 'react-icons/io5';
import { requestSolveCaptcha } from '../actions/fetch';
function getUrl() {
return `${window.ssv.captchaurl}/captcha.svg?${new Date().getTime()}`;
}
const Captcha = ({ callback, close }) => {
const [captchaUrl, setCaptchaUrl] = useState(getUrl());
const [text, setText] = useState('');
const [errors, setErrors] = useState([]);
const [imgLoaded, setImgLoaded] = useState(false);
return (
<div>
{errors.map((error) => (
<p key={error} className="errormessage">
<span>{t`Error`}</span>:&nbsp;{error}
</p>
))}
<p className="modaltext">
{t`Type the characters from the following image:`}
&nbsp;
<span style={{ fontSize: 11 }}>
({t`Tip: Not case-sensitive; I and l are the same`})
</span>
</p>
<br />
<div
style={{
width: '100%',
paddingTop: '60%',
position: 'relative',
}}
>
<img
style={{
width: '100%',
position: 'absolute',
top: '50%',
left: '50%',
opacity: (imgLoaded) ? 1 : 0,
transform: 'translate(-50%,-50%)',
transition: '100ms',
}}
src={captchaUrl}
alt="CAPTCHA"
onLoad={() => { setImgLoaded(true); }}
onError={() => setErrors([t`Could not load captcha`])}
/>
</div>
<p className="modaltext">
{t`Can't read? Reload:`}&nbsp;
<span
role="button"
tabIndex={-1}
title={t`Reload`}
className="modallink"
style={{fontSize: 28}}
onClick={() => {
setImgLoaded(false);
setCaptchaUrl(getUrl());
}}
>
<IoReloadCircleSharp />
</span>
</p>
<input
placeholder={t`Enter Characters`}
type="text"
value={text}
autoComplete="off"
style={{
width: '6em',
fontSize: 21,
margin: 5,
}}
onChange={(evt) => {
const txt = evt.target.value;
setText(txt);
if (callback) callback(txt);
}}
/>
{(!callback) && (
<div>
<button
type="button"
onClick={close}
style={{fontSize: 16}}
>
{t`Cancel`}
</button>
&nbsp;
<button
type="button"
onClick={async () => {
const { errors: resErrors } = await requestSolveCaptcha(text);
if (resErrors) {
setCaptchaUrl(getUrl());
setText('');
setErrors(resErrors);
} else {
close();
}
}}
style={{fontSize: 16}}
>
{t`Send`}
</button>
</div>
)}
</div>
);
};
export default React.memo(Captcha);

View File

@ -6,8 +6,9 @@
import React from 'react';
import { t } from 'ttag';
import {
validateEMail, validatePassword, parseAPIresponse,
validateEMail, validatePassword,
} from '../utils/validation';
import { requestMailChange } from '../actions/fetch';
function validate(email, password) {
const errors = [];
@ -20,23 +21,6 @@ function validate(email, password) {
return errors;
}
async function submitMailchange(email, password) {
const body = JSON.stringify({
email,
password,
});
const response = await fetch('./api/auth/change_mail', {
method: 'POST',
headers: {
'Content-type': 'application/json',
},
body,
credentials: 'include',
});
return parseAPIresponse(response);
}
class ChangeMail extends React.Component {
constructor() {
super();
@ -64,7 +48,7 @@ class ChangeMail extends React.Component {
if (errors.length > 0) return;
this.setState({ submitting: true });
const { errors: resperrors } = await submitMailchange(email, password);
const { errors: resperrors } = await requestMailChange(email, password);
if (resperrors) {
this.setState({
errors: resperrors,

View File

@ -5,7 +5,8 @@
import React from 'react';
import { t } from 'ttag';
import { validateName, parseAPIresponse } from '../utils/validation';
import { validateName } from '../utils/validation';
import { requestNameChange } from '../actions/fetch';
function validate(name) {
@ -17,22 +18,6 @@ function validate(name) {
return errors;
}
async function submitNamechange(name) {
const body = JSON.stringify({
name,
});
const response = await fetch('./api/auth/change_name', {
method: 'POST',
headers: {
'Content-type': 'application/json',
},
body,
credentials: 'include',
});
return parseAPIresponse(response);
}
class ChangeName extends React.Component {
constructor() {
super();
@ -58,7 +43,7 @@ class ChangeName extends React.Component {
if (errors.length > 0) return;
this.setState({ submitting: true });
const { errors: resperrors } = await submitNamechange(name);
const { errors: resperrors } = await requestNameChange(name);
if (resperrors) {
this.setState({
errors: resperrors,

View File

@ -3,9 +3,10 @@
* @flow
*/
import React from 'react';
import React, { useState } from 'react';
import { t } from 'ttag';
import { validatePassword, parseAPIresponse } from '../utils/validation';
import { validatePassword } from '../utils/validation';
import { requestPasswordChange } from '../actions/fetch';
function validate(mailreg, password, newPassword, confirmPassword) {
const errors = [];
@ -24,136 +25,92 @@ function validate(mailreg, password, newPassword, confirmPassword) {
return errors;
}
async function submitPasswordChange(newPassword, password) {
const body = JSON.stringify({
password,
newPassword,
});
const response = await fetch('./api/auth/change_passwd', {
method: 'POST',
headers: {
'Content-type': 'application/json',
},
body,
credentials: 'include',
});
const ChangePassword = ({ mailreg, done, cancel }) => {
const [password, setPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [success, setSuccess] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [errors, setErrors] = useState([]);
return parseAPIresponse(response);
}
class ChangePassword extends React.Component {
constructor() {
super();
this.state = {
password: '',
newPassword: '',
confirmPassword: '',
success: false,
submitting: false,
errors: [],
};
this.handleSubmit = this.handleSubmit.bind(this);
}
async handleSubmit(e) {
e.preventDefault();
const {
password, newPassword, confirmPassword, submitting,
} = this.state;
if (submitting) return;
const { mailreg } = this.props;
const errors = validate(
mailreg,
password,
newPassword,
confirmPassword,
);
this.setState({ errors });
if (errors.length > 0) return;
this.setState({ submitting: true });
const { errors: resperrors } = await submitPasswordChange(
newPassword,
password,
);
if (resperrors) {
this.setState({
errors: resperrors,
submitting: false,
});
return;
}
this.setState({
success: true,
});
}
render() {
const { success } = this.state;
if (success) {
const { done } = this.props;
return (
<div className="inarea">
<p className="modalmessage">{t`Changed Password successfully.`}</p>
<button type="button" onClick={done}>Close</button>
</div>
);
}
const {
errors,
password,
newPassword,
confirmPassword,
submitting,
} = this.state;
const { cancel, mailreg } = this.props;
if (success) {
return (
<div className="inarea">
<form onSubmit={this.handleSubmit}>
{errors.map((error) => (
<p key={error} className="errormessage"><span>{t`Error`}</span>
:&nbsp;{error}</p>
))}
{(mailreg)
&& (
<input
value={password}
onChange={(evt) => this.setState({ password: evt.target.value })}
type="password"
placeholder={t`Old Password`}
/>
)}
<br />
<input
value={newPassword}
onChange={(evt) => this.setState({ newPassword: evt.target.value })}
type="password"
placeholder={t`New Password`}
/>
<br />
<input
value={confirmPassword}
onChange={(evt) => this.setState({
confirmPassword: evt.target.value,
})}
type="password"
placeholder={t`Confirm New Password`}
/>
<br />
<button type="submit">
{(submitting) ? '...' : t`Save`}
</button>
<button type="button" onClick={cancel}>{t`Cancel`}</button>
</form>
<p className="modalmessage">{t`Changed Password successfully.`}</p>
<button type="button" onClick={done}>Close</button>
</div>
);
}
}
export default ChangePassword;
return (
<div className="inarea">
<form
onSubmit={async (e) => {
e.preventDefault();
if (submitting) return;
const valerrors = validate(
mailreg,
password,
newPassword,
confirmPassword,
);
setErrors(valerrors);
if (valerrors.length) return;
setSubmitting(true);
const { errors: resperrors } = await requestPasswordChange(
newPassword,
password,
);
if (resperrors) {
setErrors(resperrors);
setSubmitting(false);
return;
}
setSuccess(true);
}}
>
{errors.map((error) => (
<p key={error} className="errormessage"><span>{t`Error`}</span>
:&nbsp;{error}</p>
))}
{(mailreg)
&& (
<input
value={password}
onChange={(evt) => setPassword(evt.target.value)}
type="password"
placeholder={t`Old Password`}
/>
)}
<br />
<input
value={newPassword}
onChange={(evt) => setNewPassword(evt.target.value)}
type="password"
placeholder={t`New Password`}
/>
<br />
<input
value={confirmPassword}
onChange={(evt) => setConfirmPassword(evt.target.value)}
type="password"
placeholder={t`Confirm New Password`}
/>
<br />
<button
type="submit"
>
{(submitting) ? '...' : t`Save`}
</button>
<button
type="button"
onClick={cancel}
>
{t`Cancel`}
</button>
</form>
</div>
);
};
export default React.memo(ChangePassword);

View File

@ -4,21 +4,21 @@
*/
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import type { State } from '../reducers';
import useWindowSize from '../utils/reactHookResize';
import { showChatModal } from '../actions';
import Chat from './Chat';
function ChatBox({
chatOpen,
triggerModal,
}) {
const ChatBox = () => {
const [render, setRender] = useState(false);
const chatOpen = useSelector((state) => state.modal.chatOpen);
const dispatch = useDispatch();
useEffect(() => {
window.setTimeout(() => {
if (chatOpen) setRender(true);
@ -31,7 +31,7 @@ function ChatBox({
const { width } = useWindowSize();
if (width < 604 && chatOpen) {
triggerModal();
dispatch(showChatModal(true));
}
return (
@ -44,19 +44,6 @@ function ChatBox({
</div>
)
);
}
};
function mapStateToProps(state: State) {
const { chatOpen } = state.modal;
return { chatOpen };
}
function mapDispatchToProps(dispatch) {
return {
triggerModal() {
dispatch(showChatModal(true));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatBox);
export default React.memo(ChatBox);

View File

@ -34,7 +34,7 @@ const DailyRankings = ({ totalDailyRanking }) => (
);
function mapStateToProps(state: State) {
const { totalDailyRanking } = state.user;
const { totalDailyRanking } = state.ranks;
return { totalDailyRanking };
}

View File

@ -7,7 +7,8 @@ import React from 'react';
import { connect } from 'react-redux';
import { t } from 'ttag';
import { validatePassword, parseAPIresponse } from '../utils/validation';
import { validatePassword } from '../utils/validation';
import { requestDeleteAccount } from '../actions/fetch';
import { logoutUser } from '../actions';
function validate(password) {
@ -19,22 +20,6 @@ function validate(password) {
return errors;
}
async function submitDeleteAccount(password) {
const body = JSON.stringify({
password,
});
const response = await fetch('./api/auth/delete_account', {
method: 'POST',
headers: {
'Content-type': 'application/json',
},
body,
credentials: 'include',
});
return parseAPIresponse(response);
}
class DeleteAccount extends React.Component {
constructor() {
super();
@ -60,7 +45,7 @@ class DeleteAccount extends React.Component {
if (errors.length > 0) return;
this.setState({ submitting: true });
const { errors: resperrors } = await submitDeleteAccount(password);
const { errors: resperrors } = await requestDeleteAccount(password);
if (resperrors) {
this.setState({
errors: resperrors,

View File

@ -31,11 +31,6 @@ const HelpModal = () => {
const bindShift = <kbd> {c('keybinds').t`Shift`}</kbd>;
const bindC = <kbd>{c('keybinds').t`C`}</kbd>;
const hCaptchaPP = <a href="https://hcaptcha.com/privacy">{t`Privacy Policy`}</a>;
const reCaptchaPP = <a href="https://policies.google.com/privacy">{t`Privacy Policy`}</a>;
const hCaptchaTOS = <a href="https://hcaptcha.com/terms">{t`Terms of Service`}</a>;
const reCaptchaTOS = <a href="https://policies.google.com/terms">{t`Terms of Service`}</a>;
const guildedLink = <a href="https://pixelplanet.fun/guilded">guilded</a>;
const getIPLink = <a href="https://www.whatismyip.com/">{t`your IP`}</a>;
const mailLink = <a href="mailto:pixelplanetdev@gmail.com">pixelplanetdev@gmail.com</a>;
@ -90,20 +85,6 @@ can be downloaded from mega.nz here: `}<a href="https://mega.nz/#!JpkBwAbJ!EnSLl
{jt`Click ${mouseSymbol} middle mouse button or ${touchSymbol} long-tap to select current hovering color`}<br />
</div>
<p>{t`Partners:`} <a href="https://www.crazygames.com/c/io" target="_blank" rel="noopener noreferrer">crazygames.com</a></p>
{ (typeof window.hcaptcha === 'undefined')
? (
<p className="modaltext">
<small>
{jt`This site is protected by reCAPTCHA and the Google ${reCaptchaPP} and ${reCaptchaTOS} apply.`}
</small>
</p>
) : (
<p className="modaltext">
<small>
{jt`This site is protected by hCAPTCHA and its ${hCaptchaPP} and ${hCaptchaTOS} apply.`}
</small>
</p>
)}
</p>
);
};

View File

@ -7,8 +7,9 @@ import { connect } from 'react-redux';
import { t } from 'ttag';
import {
validateEMail, validateName, validatePassword, parseAPIresponse,
validateEMail, validateName, validatePassword,
} from '../utils/validation';
import { requestLogin } from '../actions/fetch';
import { loginUser } from '../actions';
@ -24,22 +25,6 @@ function validate(nameoremail, password) {
return errors;
}
async function submitLogin(nameoremail, password) {
const body = JSON.stringify({
nameoremail,
password,
});
const response = await fetch('./api/auth/local', {
method: 'POST',
body,
headers: {
'Content-Type': 'application/json',
},
});
return parseAPIresponse(response);
}
const inputStyles = {
display: 'inline-block',
width: '100%',
@ -73,7 +58,7 @@ class LogInForm extends React.Component {
if (errors.length > 0) return;
this.setState({ submitting: true });
const { errors: resperrors, me } = await submitLogin(
const { errors: resperrors, me } = await requestLogin(
nameoremail,
password,
);

View File

@ -5,9 +5,8 @@
* @flow
*/
import React from 'react';
import Modal from 'react-modal';
import { connect } from 'react-redux';
import React, { useState, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { MdClose } from 'react-icons/md';
import { t } from 'ttag';
@ -38,44 +37,62 @@ const MODAL_COMPONENTS = {
/* other modals */
};
const ModalRoot = ({ modalType, modalOpen, close }) => {
const choice = MODAL_COMPONENTS[modalType || 'NONE'];
const { content: SpecificModal, title } = choice;
const ModalRoot = () => {
const [render, setRender] = useState(false);
const {
modalType,
modalOpen,
} = useSelector((state) => state.modal);
const {
title,
content: SpecificModal,
} = MODAL_COMPONENTS[modalType || 'NONE'];
const dispatch = useDispatch();
const close = useCallback(() => {
dispatch(hideModal());
}, [dispatch]);
const onTransitionEnd = () => {
if (!modalOpen) setRender(false);
};
useEffect(() => {
window.setTimeout(() => {
if (modalOpen) setRender(true);
}, 10);
}, [modalOpen]);
return (
<Modal
isOpen={modalOpen}
onClose={close}
className="Modal"
overlayClassName="Overlay"
contentLabel={`${title} Modal`}
closeTimeoutMS={200}
onRequestClose={close}
>
<h2 style={{ paddingLeft: '5%' }}>{title}</h2>
<div
onClick={close}
className="ModalClose"
role="button"
label="close"
title={t`Close`}
tabIndex={-1}
><MdClose /></div>
<SpecificModal />
</Modal>
(render || modalOpen) && (
<div>
<div
className={(modalOpen && render)
? 'OverlayModal show'
: 'OverlayModal'}
onTransitionEnd={onTransitionEnd}
tabIndex={-1}
onClick={close}
/>
<div
className={(modalOpen && render) ? 'Modal show' : 'Modal'}
>
<h2 style={{ paddingLeft: '5%' }}>{title}</h2>
<div
onClick={close}
className="ModalClose"
role="button"
label="close"
title={t`Close`}
tabIndex={-1}
><MdClose /></div>
<SpecificModal />
</div>
</div>
)
);
};
function mapStateToProps(state: State) {
const { modalType, modalOpen } = state.modal;
return { modalType, modalOpen };
}
function mapDispatchToProps(dispatch) {
return {
close() {
dispatch(hideModal());
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot);
export default React.memo(ModalRoot);

View File

@ -4,7 +4,8 @@
*/
import React from 'react';
import { t } from 'ttag';
import { validateEMail, parseAPIresponse } from '../utils/validation';
import { validateEMail } from '../utils/validation';
import { requestNewPassword } from '../actions/fetch';
function validate(email) {
const errors = [];
@ -13,21 +14,6 @@ function validate(email) {
return errors;
}
async function submitNewpass(email) {
const body = JSON.stringify({
email,
});
const response = await fetch('./api/auth/restore_password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
});
return parseAPIresponse(response);
}
const inputStyles = {
display: 'inline-block',
width: '100%',
@ -60,7 +46,7 @@ class NewPasswordForm extends React.Component {
if (errors.length > 0) return;
this.setState({ submitting: true });
const { errors: resperrors } = await submitNewpass(email);
const { errors: resperrors } = await requestNewPassword(email);
if (resperrors) {
this.setState({
errors: resperrors,

View File

@ -7,8 +7,9 @@ import React from 'react';
import { connect } from 'react-redux';
import { t } from 'ttag';
import {
validateEMail, validateName, validatePassword, parseAPIresponse,
validateEMail, validateName, validatePassword,
} from '../utils/validation';
import { requestRegistration } from '../actions/fetch';
import { showUserAreaModal, loginUser } from '../actions';
@ -28,25 +29,6 @@ function validate(name, email, password, confirmPassword) {
return errors;
}
async function submitRegistration(name, email, password) {
const body = JSON.stringify({
name,
email,
password,
});
const response = await fetch('./api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
credentials: 'include',
});
return parseAPIresponse(response);
}
const inputStyles = {
display: 'inline-block',
width: '100%',
@ -83,7 +65,7 @@ class SignUpForm extends React.Component {
if (errors.length > 0) return;
this.setState({ submitting: true });
const { errors: resperrors, me } = await submitRegistration(
const { errors: resperrors, me } = await requestRegistration(
name,
email,
password,
@ -121,6 +103,7 @@ class SignUpForm extends React.Component {
<input
style={inputStyles}
value={name}
autoComplete="username"
onChange={(evt) => this.setState({ name: evt.target.value })}
type="text"
placeholder={t`Name`}
@ -128,6 +111,7 @@ class SignUpForm extends React.Component {
<input
style={inputStyles}
value={email}
autoComplete="email"
onChange={(evt) => this.setState({ email: evt.target.value })}
type="text"
placeholder={t`Email`}
@ -135,6 +119,7 @@ class SignUpForm extends React.Component {
<input
style={inputStyles}
value={password}
autoComplete="new-password"
onChange={(evt) => this.setState({ password: evt.target.value })}
type="password"
placeholder={t`Password`}
@ -142,6 +127,7 @@ class SignUpForm extends React.Component {
<input
style={inputStyles}
value={confirmPassword}
autoComplete="new-password"
onChange={(evt) => this.setState({
confirmPassword: evt.target.value,
})}

View File

@ -1,36 +1,20 @@
import React, { Component } from 'react';
import React from 'react';
class Tab extends Component {
onClick = () => {
const { label, onClick } = this.props;
onClick(label);
const Tab = ({ onClick, activeTab, label }) => {
let className = 'tab-list-item';
if (activeTab === label) {
className += ' tab-list-active';
}
render() {
const {
onClick,
props: {
activeTab,
label,
},
} = this;
let className = 'tab-list-item';
if (activeTab === label) {
className += ' tab-list-active';
}
return (
<li
role="presentation"
className={className}
onClick={onClick}
>
{label}
</li>
);
}
}
return (
<li
role="presentation"
className={className}
onClick={() => onClick(label)}
>
{label}
</li>
);
};
export default Tab;

View File

@ -1,62 +1,39 @@
import React, { Component } from 'react';
import React, { useState } from 'react';
import Tab from './Tab';
class Tabs extends Component {
constructor(props) {
super(props);
const Tabs = ({ children }) => {
const [activeTab, setActiveTab] = useState(children[0].props.label);
const { children } = this.props;
this.state = {
activeTab: children[0].props.label,
};
}
return (
<div className="tabs">
<ol className="tab-list">
{children.map((child) => {
if (!child.props) {
return undefined;
}
const { label } = child.props;
onClickTabItem = (tab) => {
this.setState({ activeTab: tab });
}
render() {
const {
onClickTabItem,
props: {
children,
},
state: {
activeTab,
},
} = this;
return (
<div className="tabs">
<ol className="tab-list">
{children.map((child) => {
if (!child.props) {
return undefined;
}
const { label } = child.props;
return (
<Tab
activeTab={activeTab}
key={label}
label={label}
onClick={onClickTabItem}
/>
);
})}
</ol>
<div className="tab-content">
{children.map((child) => {
if (!child.props || child.props.label !== activeTab) {
return undefined;
}
return child.props.children;
})}
</div>
return (
<Tab
activeTab={activeTab}
key={label}
label={label}
onClick={(tab) => setActiveTab(tab)}
/>
);
})}
</ol>
<div className="tab-content">
{children.map((child) => {
if (!child.props || child.props.label !== activeTab) {
return undefined;
}
return child.props.children;
})}
</div>
);
}
}
</div>
);
};
export default Tabs;

View File

@ -34,7 +34,7 @@ const TotalRankings = ({ totalRanking }) => (
);
function mapStateToProps(state: State) {
const { totalRanking } = state.user;
const { totalRanking } = state.ranks;
return { totalRanking };
}

View File

@ -12,6 +12,7 @@ import NotifyBox from './NotifyBox';
import GlobeButton from './GlobeButton';
import PalselButton from './PalselButton';
import Palette from './Palette';
import Alert from './Alert';
import HistorySelect from './HistorySelect';
import Mobile3DControls from './Mobile3DControls';
import UserContextMenu from './UserContextMenu';
@ -41,13 +42,14 @@ const UI = ({
}
return (
<div>
<Alert />
<PalselButton />
<Palette />
{(is3D) ? null : <GlobeButton />}
{(is3D && isOnMobile) ? <Mobile3DControls /> : null}
{(!is3D) && <GlobeButton />}
{(is3D && isOnMobile) && <Mobile3DControls />}
<CoolDownBox />
<NotifyBox />
{(menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null}
{(menuOpen && menuType) && CONTEXT_MENUS[menuType]}
</div>
);
};

View File

@ -183,25 +183,6 @@ class UserArea extends React.Component {
done={() => { this.setState({ socialSettingsExtended: false }); }}
/>
)}
{(typeof window.hcaptcha !== 'undefined')
&& (
<img
role="presentation"
src="hcaptcha.svg"
alt="hCaptcha"
title="test hCaptcha"
onClick={() => {
window.pixel = null;
window.hcaptcha.execute();
}}
style={{
width: '5%',
height: '5%',
paddingTop: 20,
cursor: 'pointer',
}}
/>
)}
</p>
);
}
@ -212,11 +193,13 @@ function mapStateToProps(state: State) {
const {
name,
mailreg,
} = state.user;
const {
totalPixels,
dailyTotalPixels,
ranking,
dailyRanking,
} = state.user;
} = state.ranks;
const stats = {
totalPixels,
dailyTotalPixels,

View File

@ -6,8 +6,8 @@ import React from 'react';
import { connect } from 'react-redux';
import { t } from 'ttag';
import { parseAPIresponse } from '../utils/validation';
import { setMinecraftName, remFromMessages } from '../actions';
import { requestResendVerify, requestMcLink } from '../actions/fetch';
class UserMessages extends React.Component {
@ -31,11 +31,8 @@ class UserMessages extends React.Component {
resentVerify: true,
});
const response = await fetch('./api/auth/resend_verify', {
credentials: 'include',
});
const { errors } = await requestResendVerify();
const { errors } = await parseAPIresponse(response);
const verifyAnswer = (errors)
? errors[0]
: t`A new verification mail is getting sent to you.`;
@ -50,15 +47,9 @@ class UserMessages extends React.Component {
this.setState({
sentLink: true,
});
const body = JSON.stringify({ accepted });
const rep = await fetch('./api/auth/mclink', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
credentials: 'include',
});
const { errors } = parseAPIresponse(rep);
const { errors } = await requestMcLink(accepted);
if (errors) {
this.setState({
linkAnswer: errors[0],

View File

@ -10,6 +10,7 @@ if (process.env.BROWSER) {
}
export const PORT = process.env.PORT || 80;
export const HOST = process.env.HOST || 'localhost';
export const GMAIL_USER = process.env.GMAIL_USER || null;
export const GMAIL_PW = process.env.GMAIL_PW || null;
@ -19,6 +20,8 @@ export const TILE_FOLDER = path.join(__dirname, `./${TILE_FOLDER_REL}`);
export const ASSET_SERVER = process.env.ASSET_SERVER || '.';
export const CAPTCHA_URL = process.env.CAPTCHA_URL || '';
export const USE_XREALIP = process.env.USE_XREALIP || false;
export const BACKUP_URL = process.env.BACKUP_URL || null;
@ -35,8 +38,6 @@ export const MYSQL_USER = process.env.MYSQL_USER || 'pixelplanet';
export const MYSQL_PW = process.env.MYSQL_PW || 'password';
// Social
export const DISCORD_INVITE = process.env.DISCORD_INVITE
|| 'https://discordapp.com/';
export const GUILDED_INVITE = process.env.GUILDED_INVITE
|| 'https://www.guilded.gg/';
@ -87,13 +88,9 @@ export const auth = {
},
};
// o: none
// 1: reCaptcha
// 2: hCaptcha
export const CAPTCHA_METHOD = Number(process.env.CAPTCHA_METHOD || 0);
export const CAPTCHA_SECRET = process.env.CAPTCHA_SECRET || false;
export const CAPTCHA_SITEKEY = process.env.CAPTCHA_SITEKEY || false;
// time on which to display captcha in minutes
export const CAPTCHA_TIME = parseInt(process.env.CAPTCHA_TIME, 10) || 30;
// time during which the user can solve a captcha in seconds
export const CAPTCHA_TIMEOUT = parseInt(process.env.CAPTCHA_TIMEOUT, 10) || 120;
export const SESSION_SECRET = process.env.SESSION_SECRET || 'dummy';

51
src/reducers/alert.js Normal file
View File

@ -0,0 +1,51 @@
/* @flow */
import type { Action } from '../actions/types';
export type AlertState = {
alertOpen: boolean,
alertType: ?string,
alertTitle: ?string,
alertMessage: ?string,
alertBtn: ?string,
};
const initialState: AlertState = {
alertOpen: false,
alertType: null,
alertTitle: null,
alertMessage: null,
alertBtn: null,
};
export default function alert(
state: AlertState = initialState,
action: Action,
): AlertState {
switch (action.type) {
case 'ALERT': {
const {
title, text, icon, confirmButtonText,
} = action;
return {
...state,
alertOpen: true,
alertTitle: title,
alertMessage: text,
alertType: icon,
alertBtn: confirmButtonText,
};
}
case 'CLOSE_ALERT': {
return {
...state,
alertOpen: false,
};
}
default:
return state;
}
}

View File

@ -7,6 +7,8 @@ import canvas from './canvas';
import gui from './gui';
import modal from './modal';
import user from './user';
import ranks from './ranks';
import alert from './alert';
import chat from './chat';
import contextMenu from './contextMenu';
import chatRead from './chatRead';
@ -17,6 +19,8 @@ import type { CanvasState } from './canvas';
import type { GUIState } from './gui';
import type { ModalState } from './modal';
import type { UserState } from './user';
import type { RanksState } from './ranks';
import type { AlertState } from './alert';
import type { ChatState } from './chat';
import type { ContextMenuState } from './contextMenu';
import type { FetchingState } from './fetching';
@ -27,6 +31,8 @@ export type State = {
gui: GUIState,
modal: ModalState,
user: UserState,
ranks: RanksState,
alert: AlertState,
chat: ChatState,
contextMenu: ContextMenuState,
chatRead: ChatReadState,
@ -38,7 +44,9 @@ const config = {
storage: localForage,
blacklist: [
'user',
'ranks',
'canvas',
'alert',
'modal',
'chat',
'contextMenu',
@ -52,6 +60,8 @@ export default persistCombineReducers(config, {
gui,
modal,
user,
ranks,
alert,
chat,
contextMenu,
chatRead,

80
src/reducers/ranks.js Normal file
View File

@ -0,0 +1,80 @@
/* @flow */
import type { Action } from '../actions/types';
export type RanksState = {
totalPixels: number,
dailyTotalPixels: number,
ranking: number,
dailyRanking: number,
// global stats
online: ?number,
totalRanking: Object,
totalDailyRanking: Object,
};
const initialState: RanksState = {
totalPixels: 0,
dailyTotalPixels: 0,
ranking: 1488,
dailyRanking: 1488,
online: 1,
totalRanking: {},
totalDailyRanking: {},
};
export default function ranks(
state: RanksState = initialState,
action: Action,
): RanksState {
switch (action.type) {
case 'PLACED_PIXELS': {
let { totalPixels, dailyTotalPixels } = state;
const { amount } = action;
totalPixels += amount;
dailyTotalPixels += amount;
return {
...state,
totalPixels,
dailyTotalPixels,
};
}
case 'RECEIVE_ONLINE': {
const { online } = action;
return {
...state,
online,
};
}
case 'RECEIVE_ME':
case 'LOGIN': {
const {
totalPixels,
dailyTotalPixels,
ranking,
dailyRanking,
} = action;
return {
...state,
totalPixels,
dailyTotalPixels,
ranking,
dailyRanking,
};
}
case 'RECEIVE_STATS': {
const { totalRanking, totalDailyRanking } = action;
return {
...state,
totalRanking,
totalDailyRanking,
};
}
default:
return state;
}
}

View File

@ -5,7 +5,6 @@ import type { Action } from '../actions/types';
import { createNameRegExp } from '../core/utils';
export type UserState = {
name: string,
center: Cell,
@ -13,18 +12,9 @@ export type UserState = {
coolDown: ?number, // ms
lastCoolDownEnd: ?Date,
requestingPixel: boolean,
online: ?number,
// messages are sent by api/me, like not_verified status
messages: Array,
mailreg: boolean,
// stats
totalPixels: number,
dailyTotalPixels: number,
ranking: number,
dailyRanking: number,
// global stats
totalRanking: Object,
totalDailyRanking: Object,
// minecraft
minecraftname: string,
// blocking all Dms
@ -46,11 +36,8 @@ const initialState: UserState = {
coolDown: null,
lastCoolDownEnd: null,
requestingPixel: true,
online: null,
messages: [],
mailreg: false,
totalRanking: {},
totalDailyRanking: {},
minecraftname: null,
blockDm: false,
isOnMobile: false,
@ -81,7 +68,7 @@ export default function user(
};
}
case 'SET_PLACE_ALLOWED': {
case 'SET_REQUESTING_PIXEL': {
const { requestingPixel } = action;
return {
...state,
@ -120,35 +107,11 @@ export default function user(
};
}
case 'PLACED_PIXELS': {
let { totalPixels, dailyTotalPixels } = state;
const { amount } = action;
totalPixels += amount;
dailyTotalPixels += amount;
return {
...state,
totalPixels,
dailyTotalPixels,
};
}
case 'RECEIVE_ONLINE': {
const { online } = action;
return {
...state,
online,
};
}
case 'RECEIVE_ME':
case 'LOGIN': {
const {
name,
mailreg,
totalPixels,
dailyTotalPixels,
ranking,
dailyRanking,
minecraftname,
blockDm,
userlvl,
@ -160,10 +123,6 @@ export default function user(
name,
messages,
mailreg,
totalPixels,
dailyTotalPixels,
ranking,
dailyRanking,
minecraftname,
blockDm,
userlvl,
@ -184,15 +143,6 @@ export default function user(
};
}
case 'RECEIVE_STATS': {
const { totalRanking, totalDailyRanking } = action;
return {
...state,
totalRanking,
totalDailyRanking,
};
}
case 'SET_NAME': {
const { name } = action;
const nameRegExp = createNameRegExp(name);

View File

@ -8,42 +8,51 @@
import type { Request, Response } from 'express';
import logger from '../../core/logger';
import { verifyCaptcha } from '../../utils/captcha';
import { checkCaptchaSolution } from '../../utils/captcha';
import { getIPFromRequest } from '../../utils/ip';
export default async (req: Request, res: Response) => {
const ip = getIPFromRequest(req);
const { t } = req.ttag;
try {
const { token } = req.body;
if (!token) {
const { text } = req.body;
if (!text) {
res.status(400)
.json({ errors: [{ msg: 'No token given' }] });
.json({ errors: [t`No captcha text given`] });
return;
}
if (!await verifyCaptcha(token, ip)) {
logger.info(`CAPTCHA ${ip} failed his captcha`);
res.status(422)
.json({
errors: [{
msg:
'You failed your captcha',
}],
});
return;
}
const ret = await checkCaptchaSolution(text, ip);
res.status(200)
.json({ success: true });
switch (ret) {
case 0:
res.status(200)
.json({ success: true });
break;
case 1:
res.status(422)
.json({
errors: [t`You took too long, try again.`],
});
break;
case 2:
res.status(422)
.json({
errors: [t`You failed your captcha`],
});
break;
default:
res.status(422)
.json({
errors: [t`Unknown Captcha Error`],
});
}
} catch (error) {
logger.error('checkHuman', error);
logger.error('CAPTCHA', error);
res.status(500)
.json({
errors: [{
msg:
'Server error occured',
}],
errors: [t`Server error occured`],
});
}
};

View File

@ -44,6 +44,11 @@ router.use((err, req, res, next) => {
}
});
/*
* make localisations available
*/
router.use(expressTTag);
// captcah doesn't need a user
router.post('/captcha', captcha);
@ -89,11 +94,6 @@ router.post('/block', block);
router.post('/blockdm', blockdm);
/*
* make localisations available
*/
router.use(expressTTag);
router.get('/chathistory', chatHistory);
router.get('/me', me);

View File

@ -11,7 +11,6 @@
/* eslint-disable max-len */
import React from 'react';
import { CAPTCHA_METHOD, CAPTCHA_SITEKEY } from '../core/config';
const Html = ({
title,
@ -25,8 +24,6 @@ const Html = ({
styles,
// code as string
code,
// if recaptcha should get loaded
useCaptcha,
}) => (
<html className="no-js" lang="en">
<head>
@ -48,10 +45,6 @@ const Html = ({
dangerouslySetInnerHTML={{ __html: style.cssText }}
/>
))}
{(CAPTCHA_METHOD === 1) && CAPTCHA_SITEKEY && useCaptcha
&& <script src="https://www.google.com/recaptcha/api.js" async defer />}
{(CAPTCHA_METHOD === 2) && CAPTCHA_SITEKEY && useCaptcha
&& <script src="https://hcaptcha.com/1/api.js" async defer />}
{code && (
<script
// eslint-disable-next-line react/no-danger
@ -67,24 +60,6 @@ const Html = ({
{body}
</div>
{scripts && scripts.map((script) => <script key={script} src={script} />)}
{(CAPTCHA_METHOD === 2) && CAPTCHA_SITEKEY && useCaptcha
&& (
<div
className="h-captcha"
data-sitekey={CAPTCHA_SITEKEY}
data-callback="onCaptcha"
data-size="invisible"
/>
)}
{(CAPTCHA_METHOD === 1) && CAPTCHA_SITEKEY && useCaptcha
&& (
<div
className="g-recaptcha"
data-sitekey={CAPTCHA_SITEKEY}
data-callback="onCaptcha"
data-size="invisible"
/>
)}
</body>
</html>
);

View File

@ -17,7 +17,7 @@ import assets from './assets.json';
// eslint-disable-next-line import/no-unresolved
import styleassets from './styleassets.json';
import { ASSET_SERVER, BACKUP_URL } from '../core/config';
import { CAPTCHA_URL, ASSET_SERVER, BACKUP_URL } from '../core/config';
/*
* generate language list
@ -31,6 +31,7 @@ const langs = Object.keys(ttags)
*/
const ssv = {
assetserver: ASSET_SERVER,
captchaurl: CAPTCHA_URL,
availableStyles: styleassets,
langs,
};

View File

@ -5,7 +5,6 @@ import thunk from 'redux-thunk';
import { persistStore } from 'redux-persist';
import audio from './audio';
import swal from './sweetAlert';
import protocolClientHook from './protocolClientHook';
import rendererHook from './rendererHook';
// import ads from './ads';
@ -13,6 +12,7 @@ import array from './array';
import promise from './promise';
import notifications from './notifications';
import title from './title';
import placePixelControl from './placePixelControl';
import extensions from './extensions';
import reducers from '../reducers';
@ -25,12 +25,12 @@ const store = createStore(
thunk,
promise,
array,
swal,
audio,
notifications,
title,
protocolClientHook,
rendererHook,
placePixelControl,
extensions,
),
),

View File

@ -0,0 +1,20 @@
/*
* Hooks for placePixel
*
* @flow
*/
import { requestFromQueue } from '../ui/placePixel';
export default (store) => (next) => (action) => {
switch (action.type) {
case 'CLOSE_ALERT': {
requestFromQueue(store);
break;
}
default:
// nothing
}
return next(action);
};

View File

@ -47,7 +47,7 @@ export default (store) => (next) => (action) => {
break;
}
case 'SET_PLACE_ALLOWED': {
case 'SET_REQUESTING_PIXEL': {
const renderer = getRenderer();
renderer.forceNextSubRender = true;
break;

View File

@ -1,30 +0,0 @@
/*
* @flow
*/
import swal from 'sweetalert2';
export default () => (next) => (action) => {
switch (action.type) {
case 'ALERT': {
const {
title,
text,
icon,
confirmButtonText,
} = action;
swal.fire({
title,
text,
icon,
confirmButtonText,
});
break;
}
default:
// nothing
}
return next(action);
};

View File

@ -112,6 +112,8 @@ td, th {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
max-width: 18em;
overflow-x: hidden;
}
tr:nth-child(even) {
@ -322,7 +324,7 @@ tr:nth-child(even) {
padding: 0;
}
.Modal {
.Modal, .Alert {
position: fixed;
top: 50%;
left: 50%;
@ -334,14 +336,26 @@ tr:nth-child(even) {
border-radius: 4px;
outline: currentcolor none medium;
transform: translate(-50%, -50%);
transition: opacity 200ms ease-in-out;
opacity: 0;
}
.Modal {
height: 80%;
max-height: 900px;
width: 70%;
transition: all 0.5s ease 0s;
max-height: 900px;
z-index: 5;
}
.Alert {
max-height: 100%;
padding: 15px;
text-align: center;
z-index: 7;
}
.modaltext, .modalcotext {
color: hsla(218, 5%, 47%, .6);
color: hsla(0, 0%, 0%, 0.6);
font-size: 14px;
font-weight: 500;
line-height: normal;
@ -370,7 +384,7 @@ tr:nth-child(even) {
.modaldesc {
box-sizing: border-box;
flex: 1 1 auto;
color: hsla(218, 5%, 47%, .6);
color: hsla(0, 0%, 0%, 0.6);
font-size: 14px;
line-height: 20px;
font-weight: 500;
@ -397,7 +411,7 @@ tr:nth-child(even) {
}
.modalcvtext {
color: hsla(218, 5%, 47%, .6);
color: hsla(0, 0%, 0%, 0.6);
font-size: 14px;
font-weight: 500;
padding: 0;
@ -432,7 +446,7 @@ tr:nth-child(even) {
}
@media (max-width: 604px) {
.Modal {
.Modal, .Alert {
position: fixed;
top: 0px;
left: 0px;
@ -446,16 +460,26 @@ tr:nth-child(even) {
padding: 5%;
}
}
.Overlay {
.OverlayModal, .OverlayAlert {
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(255, 255, 255, 0.75);
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.OverlayModal {
z-index: 4;
}
.OverlayAlert {
z-index: 6;
}
.chatbox div .chatarea {
height: 174px;
}
@ -697,15 +721,6 @@ tr:nth-child(even) {
visibility: hidden;
}
.ReactModal__Overlay {
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.ReactModal__Overlay--after-open{
.Modal.show, .Alert.show, .OverlayAlert.show, .OverlayModal.show {
opacity: 1;
}
.ReactModal__Overlay--before-close{
opacity: 0;
}

View File

@ -11,7 +11,7 @@ tr:nth-child(odd) {
border-radius: 10px;
}
.Overlay {
.OverlayModal, .OverlayAlert {
background: linear-gradient(175deg, #61dceaab , #ffb1e1, #ecffec, #ffb1e1, #61dceaab);
}
@ -54,3 +54,8 @@ tr:nth-child(odd) {
background-size: cover;
border-radius: 10px;
}
.Alert {
background: #f4edf0 none repeat scroll 0 0;
border-radius: 7px;
}

View File

@ -65,12 +65,19 @@ tr:nth-child(even) {
background-color: #15374fd1;
}
.Modal {
.Modal, .Alert {
background: #444242 none repeat scroll 0 0;;
color: #f4f4f4;
}
.Modal {
border-radius: 21px;
}
.Alert {
border-radius: 12px;
}
.modaltext, .modalcotext {
color: #f4f4f4;
}
@ -104,7 +111,7 @@ tr:nth-child(even) {
background-color: #6f6f75;
}
.Overlay {
.OverlayModal, .OverlayAlert {
background-color: rgba(187, 187, 187, 0.75);
}

View File

@ -0,0 +1,149 @@
a:link {
color: #91ffe0;
}
a:visited {
color: #b5d06d;
}
a:hover {
color: #d9f68a;
}
.modallink {
color: #91ffe0;
}
.modallink:hover {
color: #d9f68a;
}
.inarea {
border-color: #d5d5d5;
}
.tab-list-active {
background-color: #7b7b7b;
}
tr:nth-child(even) {
background-color: #505050;
}
.chatbox {
background-color: rgba(59, 59, 59, 0.8);
border-radius: 4px;
}
.channeldd, .contextmenu {
background-color: #353535;
color: #efefef;
border-radius: 4px;
}
.chntop {
margin-top: 4px;
}
.chn, .chntype, .contextmenu > div {
background-color: #353535;
}
.chn.selected, .chn:hover, .chntype.selected, .chntype:hover,
.contextmenu > div:hover {
background-color: #404040;
}
.actionbuttons, .coorbox, .onlinebox, .cooldownbox, #historyselect {
background-color: rgba(27, 27, 27, 0.8);
color: #ffd4ae;
border-radius: 15px;
}
.menu > div {
z-index: 1;
background-color: #000000d1;
}
.contextmenu > div {
border-top: thin solid #ffa14c;
}
.Modal, .Alert {
background: #262626 none repeat scroll 0 0;;
color: #ff8c22;
}
.Modal {
border-radius: 15px;
}
.Alert {
border-radius: 10px;
}
.modaltext, .modalcotext {
color: #f8f8f8;
}
.modaltitle {
color: #f38826;
}
.modaldesc {
color: hsl(0, 0%, 88%);
}
.modaldivider {
background-color: hsla(216, 4%, 74%, .3);
}
.modalinfo {
color: #ddd;
}
.modalcvtext {
color: hsla(220, 100%, 95.3%, 0.6);
}
.ModalClose {
background-color: #55555d;
border-color: #dcddde;
}
.ModalClose:hover {
background-color: #6f6f75;
}
.OverlayModal, .OverlayAlert {
background-color: rgba(187, 187, 187, 0.75);
}
.chatmsg {
color: #fffcf7;
}
.msg.info{
color: #ff91a6;
}
.msg.event{
color: #9dc8ff;
}
.msg.greentext{
color: #94ff94;
}
.chatlink {
color: #f9edde;
}
.statvalue {
color: #ecc9ff;
}
.actionbuttons:hover, .coorbox:hover, .menu > div:hover {
background-color: rgba(71, 71, 71, 0.8);
}
#outstreamContainer {
background-color: black;
}

View File

@ -44,7 +44,7 @@ tr:nth-child(even) {
background-color: #15374fd1;
}
.Modal {
.Modal, .Alert {
background: #444242 none repeat scroll 0 0;;
color: #f4f4f4;
}
@ -96,7 +96,7 @@ tr:nth-child(even) {
background-color: #6f6f75;
}
.Overlay {
.OverlayModal, .OverlayAlert {
background-color: rgba(187, 187, 187, 0.75);
}

View File

@ -14,6 +14,10 @@
border-radius: 21px;
}
.Alert {
border-radius: 12px;
}
.notifybox {
border-radius: 21px;
}

View File

@ -8,7 +8,7 @@
import { t } from 'ttag';
import {
notify,
setPlaceAllowed,
setRequestingPixel,
sweetAlert,
gotCoolDownDelta,
pixelFailure,
@ -38,7 +38,7 @@ let clientPredictions = [];
let lastRequestValues = {};
function requestFromQueue(store) {
export function requestFromQueue(store) {
if (!pixelQueue.length) {
pixelTimeout = null;
return;
@ -48,7 +48,7 @@ function requestFromQueue(store) {
pixelTimeout = setTimeout(() => {
pixelQueue = [];
pixelTimeout = null;
store.dispatch(setPlaceAllowed(true));
store.dispatch(setRequestingPixel(true));
store.dispatch(sweetAlert(
t`Error :(`,
t`Didn't get an answer from pixelplanet. Maybe try to refresh?`,
@ -60,16 +60,7 @@ function requestFromQueue(store) {
lastRequestValues = pixelQueue.shift();
const { i, j, pixels } = lastRequestValues;
ProtocolClient.requestPlacePixels(i, j, pixels);
store.dispatch(setPlaceAllowed(false));
// TODO:
// this is for resending after captcha returned
// window is ugly, put it into redux or something
window.pixel = {
i,
j,
pixels,
};
store.dispatch(setRequestingPixel(false));
}
export function receivePixelUpdate(
@ -239,15 +230,15 @@ export function receivePixelReturn(
store.dispatch(pixelWait());
break;
case 10:
// captcha, reCaptcha or hCaptcha
if (typeof window.hcaptcha !== 'undefined') {
window.hcaptcha.execute();
} else {
window.grecaptcha.execute();
}
store.dispatch(sweetAlert(
'Captcha',
t`Please prove that you are human`,
'captcha',
t`OK`,
));
store.dispatch(setRequestingPixel(true));
return;
case 11:
errorTitle = t`No Proxies Allowed :(`;
msg = t`You are using a Proxy.`;
break;
@ -266,7 +257,7 @@ export function receivePixelReturn(
));
}
store.dispatch(setPlaceAllowed(true));
store.dispatch(setRequestingPixel(true));
/* start next request if queue isn't empty */
requestFromQueue(store);
}

View File

@ -3,117 +3,70 @@
* @flow
*/
import fetch from 'isomorphic-fetch';
import logger from '../core/logger';
import redis from '../data/redis';
import { getIPv6Subnet } from './ip';
import {
CAPTCHA_METHOD,
CAPTCHA_SECRET,
CAPTCHA_URL,
CAPTCHA_TIME,
CAPTCHA_TIMEOUT,
} from '../core/config';
const TTL_CACHE = CAPTCHA_TIME * 60; // seconds
// eslint-disable-next-line max-len
const RECAPTCHA_ENDPOINT = `https://www.google.com/recaptcha/api/siteverify?secret=${CAPTCHA_SECRET}`;
const HCAPTCHA_ENDPOINT = 'https://hcaptcha.com/siteverify';
/**
* https://stackoverflow.com/questions/27297067/google-recaptcha-how-to-get-user-response-and-validate-in-the-server-side
*
* @param token
* @param ip
* @returns {Promise.<boolean>}
*/
async function verifyReCaptcha(
token: string,
ip: string,
): Promise<boolean> {
const url = `${RECAPTCHA_ENDPOINT}&response=${token}&remoteip=${ip}`;
const response = await fetch(url);
if (response.ok) {
const { success } = await response.json();
if (success) {
logger.info(`CAPTCHA ${ip} successfully solved captcha`);
return true;
}
logger.info(`CAPTCHA Token for ${ip} not ok`);
} else {
logger.warn(`CAPTCHA Recapcha answer for ${ip} not ok`);
}
return false;
function captchaTextFilter(text: string) {
let ret = text.toString('utf8');
ret = ret.split('l').join('i');
ret = ret.split('0').join('O');
ret = ret.toLowerCase();
return ret;
}
/*
* https://docs.hcaptcha.com/
* set captcha solution
*
* @param token
* @param text Solution of captcha
* @param ip
* @return boolean, true if successful, false on error or fail
* @param ttl time to be valid in seconds
*/
async function verifyHCaptcha(
token: string,
export function setCaptchaSolution(
text: string,
ip: string,
): Promise<boolean> {
const response = await fetch(HCAPTCHA_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `response=${token}&secret=${CAPTCHA_SECRET}&remoteip=${ip}`,
});
if (response.ok) {
const { success } = await response.json();
if (success) {
logger.info(`CAPTCHA ${ip} successfully solved captcha`);
return true;
}
logger.info(`CAPTCHA Token for ${ip} not ok`);
} else {
// eslint-disable-next-line max-len
logger.warn(`CAPTCHA hCapcha answer for ${ip} not ok ${await response.text()}`);
}
return false;
) {
const key = `capt:${ip}`;
return redis.setAsync(key, captchaTextFilter(text), 'EX', CAPTCHA_TIMEOUT);
}
/*
* verify captcha token from client
* check captcha solution
*
* @param token token of solved captcha from client
* @param text Solution of captcha
* @param ip
* @returns Boolean if successful
* @return 0 if solution right
* 1 if timed out
* 2 if wrong
*/
export async function verifyCaptcha(
token: string,
export async function checkCaptchaSolution(
text: string,
ip: string,
): Promise<boolean> {
try {
if (!CAPTCHA_METHOD) {
return true;
) {
const ipn = getIPv6Subnet(ip);
const key = `capt:${ip}`;
let solution = await redis.getAsync(key);
if (solution) {
if (solution.toString('utf8') === captchaTextFilter(text)) {
const solvkey = `human:${ipn}`;
await redis.setAsync(solvkey, '', 'EX', TTL_CACHE);
logger.info(`CAPTCHA ${ip} successfully solved captcha`);
return 0;
}
const key = `human:${ip}`;
switch (CAPTCHA_METHOD) {
case 1:
if (!await verifyReCaptcha(token, ip)) {
return false;
}
break;
case 2:
if (!await verifyHCaptcha(token, ip)) {
return false;
}
break;
default:
// nothing
}
await redis.setAsync(key, '', 'EX', TTL_CACHE);
return true;
} catch (error) {
logger.error(error);
logger.info(
`CAPTCHA ${ip} got captcha wrong (${text} instead of ${solution})`,
);
return 2;
}
return false;
logger.info(`CAPTCHA ${ip} timed out`);
return 1;
}
/*
@ -123,11 +76,11 @@ export async function verifyCaptcha(
* @return boolean true if needed
*/
export async function needCaptcha(ip: string) {
if (!CAPTCHA_METHOD) {
if (!CAPTCHA_URL) {
return false;
}
const key = `human:${ip}`;
const key = `human:${getIPv6Subnet(ip)}`;
const ttl: number = await redis.ttlAsync(key);
if (ttl > 0) {
return false;
@ -135,6 +88,3 @@ export async function needCaptcha(ip: string) {
logger.info(`CAPTCHA ${ip} got captcha`);
return true;
}
export default verifyCaptcha;

View File

@ -1,60 +0,0 @@
/*
* check if IP is from cloudflare
* @flow
*/
import { Address4, Address6 } from 'ip-address';
import logger from '../core/logger';
// returns null | Address4 | Address6
function intoAddress(str) {
if (typeof str === 'string') str = str.trim();
try {
const isV6 = (str.indexOf(':') !== -1);
let ip = null;
if (isV6) {
ip = new Address6(str);
} else {
ip = new Address4(str);
}
return ip;
} catch {
logger.error(`CF-check: Got invalid ip ${str}`);
return null;
}
}
const cloudflareIps = [
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'104.16.0.0/12',
'108.162.192.0/18',
'131.0.72.0/22',
'141.101.64.0/18',
'162.158.0.0/15',
'172.64.0.0/13',
'173.245.48.0/20',
'188.114.96.0/20',
'190.93.240.0/20',
'197.234.240.0/22',
'198.41.128.0/17',
'2400:cb00::/32',
'2405:8100::/32',
'2405:b500::/32',
'2606:4700::/32',
'2803:f800::/32',
'2c0f:f248::/32',
'2a06:98c0::/29',
].map(intoAddress);
// returns bool
function isCloudflareIp(testIpString: string): boolean {
if (!testIpString) return false;
const testIp = intoAddress(testIpString);
if (!testIp) return false;
return cloudflareIps.some((cf) => testIp.isInSubnet(cf));
}
export default isCloudflareIp;

View File

@ -3,20 +3,11 @@
* @flow
*/
import isCloudflareIp from './cloudflareip';
import logger from '../core/logger';
import { USE_XREALIP } from '../core/config';
function isTrustedProxy(ip: string): boolean {
if (ip === '::ffff:127.0.0.1' || ip === '127.0.0.1' || isCloudflareIp(ip)) {
return true;
}
return false;
}
export function getHostFromRequest(req): ?string {
const { headers } = req;
const host = headers['x-forwarded-host'] || headers.host;
@ -26,30 +17,25 @@ export function getHostFromRequest(req): ?string {
}
export function getIPFromRequest(req): ?string {
const { socket, connection, headers } = req;
if (USE_XREALIP) {
const ip = req.headers['x-real-ip'];
if (ip) {
return ip;
}
}
const { socket, connection } = req;
let conip = (connection ? connection.remoteAddress : socket.remoteAddress);
conip = conip || '0.0.0.1';
if (USE_XREALIP) {
const ip = headers['x-real-ip'];
return ip || conip;
if (!USE_XREALIP) {
logger.warn(
`Connection not going through reverse proxy! IP: ${conip}`, req.headers,
);
}
if (!headers['x-forwarded-for'] || !isTrustedProxy(conip)) {
// eslint-disable-next-line max-len
logger.warn(`Connection not going through nginx and cloudflare! IP: ${conip}`, headers);
return conip;
}
const forwardedFor = headers['x-forwarded-for'];
const ipList = forwardedFor.split(',').map((str) => str.trim());
let ip = ipList.pop();
while (isTrustedProxy(ip) && ipList.length) {
ip = ipList.pop();
}
return ip || conip;
return conip;
}
export function getIPv6Subnet(ip: string): string {

View File

@ -61,23 +61,3 @@ export function validatePassword(password) {
}
return false;
}
/*
* makes sure that responses from the api
* includes errors when failure occures
*/
export async function parseAPIresponse(response) {
try {
const resp = await response.json();
if (!response.ok && !resp.errors) {
return {
errors: [t`Could not connect to server, please try again later :(`],
};
}
return resp;
} catch (e) {
return {
errors: [t`I think we experienced some error :(`],
};
}
}

View File

@ -31,7 +31,7 @@ import generateGlobePage from './ssr-components/Globe';
import generateMainPage from './ssr-components/Main';
import { SECOND, MONTH } from './core/constants';
import { PORT, DISCORD_INVITE, GUILDED_INVITE } from './core/config';
import { PORT, HOST, GUILDED_INVITE } from './core/config';
import { ccToCoords } from './utils/location';
import { startAllCanvasLoops } from './core/tileserver';
@ -111,11 +111,8 @@ app.use(express.static(path.join(__dirname, 'public'), {
//
// Redirecct to discord and guilded
// Redirecct to guilded
// -----------------------------------------------------------------------------
app.use('/discord', (req, res) => {
res.redirect(DISCORD_INVITE);
});
app.use('/guilded', (req, res) => {
res.redirect(GUILDED_INVITE);
});
@ -203,10 +200,13 @@ const promise = models.sync({ alter: { drop: false } })
// const promise = models.sync()
.catch((err) => logger.error(err.stack));
promise.then(() => {
server.listen(PORT, () => {
server.listen(PORT, HOST, () => {
rankings.updateRanking();
chatProvider.initialize();
const address = server.address();
logger.log('info', `web is running at http://localhost:${address.port}/`);
logger.log(
'info',
`web is running at http://${address.host}:${address.port}/`,
);
});
});

View File

@ -64,6 +64,7 @@ export default ({
entry: {
web: [path.resolve(__dirname, 'src', 'web.js')],
backup: [path.resolve(__dirname, 'src', 'backup.js')],
captchaserver: [path.resolve(__dirname, 'src', 'captchaserver.js')],
},
output: {