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-one-expression-per-line": "off",
"react/jsx-closing-tag-location":"off", "react/jsx-closing-tag-location":"off",
"jsx-a11y/click-events-have-key-events":"off", "jsx-a11y/click-events-have-key-events":"off",
"jsx-a11y/no-static-element-interactions":"off",
"no-continue": "off", "no-continue": "off",
"no-multiple-empty-lines": "off", "no-multiple-empty-lines": "off",
"lines-between-class-members":["warn", "always",{"exceptAfterSingleLine": true}] "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 | | Variable | Description | Example |
|----------------|:-------------------------|------------------------:| |----------------|:-------------------------|------------------------:|
| PORT | Port | 80 | | PORT | Own Port | 80 |
| REDIS_URL | URL:PORT of redis server | "redis://localhost:6379" | | HOST | Own Host | "localhost" |
| REDIS_URL | URL:PORT of redis server | "redis://localhost:6379"|
| MYSQL_HOST | MySql Host | "localhost" | | MYSQL_HOST | MySql Host | "localhost" |
| MYSQL_USER | MySql User | "user" | | MYSQL_USER | MySql User | "user" |
| MYSQL_PW | MySql Password | "password" | | MYSQL_PW | MySql Password | "password" |
@ -78,24 +79,22 @@ Configuration takes place in the environment variables that are defined in ecosy
#### Optional Configuration #### Optional Configuration
| Variable | Description | Example | | Variable | Description | Example |
|-------------------|:--------------------------------------|--------------------| |-------------------|:--------------------------------------|-------------------------|
| ASSET_SERVER | URL for assets | "http://localhost" | | ASSET_SERVER | URL for assets | "http://localhost" |
| USE_PROXYCHECK | Check users for Proxies | 0 | | USE_PROXYCHECK | Check users for Proxies | 0 |
| APISOCKET_KEY | Key for API Socket for SpecialAccess™ | "SDfasife3" | | APISOCKET_KEY | Key for API Socket for SpecialAccess™ | "SDfasife3" |
| ADMIN_IDS | Ids of users with Admin rights | "1,12,3" | | ADMIN_IDS | Ids of users with Admin rights | "1,12,3" |
| CAPTCHA_METHOD | 0: none, 1: reCaptcha, 2: hCaptcha | 2 | | CAPTCHA_URL | URL where captcha is served | "http://localhost:8080" |
| CAPTCHA_SECRET | re/hCaptcha secret key | "asdieewff" | | CAPTCHA_TIME | time in minutes between captchas | 30 |
| CAPTCHA_SITEKEY | re/hCaptcha site key | "23ksdfssd" | | SESSION_SECRET | random sting for express sessions | "ayylmao" |
| CAPTCHA_TIME | time in minutes between captchas | 30 | | LOG_MYSQL | if sql queries should get logged | 0 |
| SESSION_SECRET | random sting for express sessions | "ayylmao" | | USE_XREALIP | see ngins / CDN section | 1 |
| LOG_MYSQL | if sql queries should get logged | 0 | | BACKUP_URL | url of backup server (see Backup) | "http://localhost" |
| USE_XREALIP | see cloudflare section | 1 | | BACKUP_DIR | mounted directory of backup server | "/mnt/backup/" |
| BACKUP_URL | url of backup server (see Backup) | "http://localhost" | | GMAIL_USER | gmail username if used for mails | "ppfun@gmail.com" |
| BACKUP_DIR | mounted directory of backup server | "/mnt/backup/" | | GMAIL_PW | gmail password if used for mails | "lolrofls" |
| GMAIL_USER | gmail username if used for mails | "ppfun@gmail.com" | | HOURLY_EVENT | run hourly void event on main canvas | 1 |
| GMAIL_PW | gmail password if used for mails | "lolrofls" |
| HOURLY_EVENT | run hourly void event on main canvas | 1 |
Notes: Notes:
@ -108,7 +107,7 @@ Notes:
| Variable | Description | | Variable | Description |
|-----------------------|:-------------------------| |-----------------------|:-------------------------|
| DISCORD_INVITE | Invite to discord server | | GUILDED_INVITE | Invite to guilded server |
| DISCORD_CLIENT_ID | All | | DISCORD_CLIENT_ID | All |
| DISCORD_CLIENT_SECRET | those | | DISCORD_CLIENT_SECRET | those |
| GOOGLE_CLIENT_ID | values | | 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' name : 'web'
node_args: --nouse-idle-notification --expose-gc node_args: --nouse-idle-notification --expose-gc
env: env:
HOSTURL: "http://localhost"
ASSET_SERVER: "http://localhost"
PORT: 80 PORT: 80
HOST: "localhost"
ASSET_SERVER: "http://localhost"
REDIS_URL: 'redis://localhost:6379' REDIS_URL: 'redis://localhost:6379'
MYSQL_HOST: "localhost" MYSQL_HOST: "localhost"
MYSQL_USER: "pixelplanet" MYSQL_USER: "pixelplanet"

View File

@ -42,8 +42,10 @@ do
cd "$PFOLDER" cd "$PFOLDER"
pm2 stop web pm2 stop web
pm2 stop backup pm2 stop backup
pm2 stop captchas
pm2 start ecosystem.yml pm2 start ecosystem.yml
pm2 start ecosystem-backup.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" 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 else
echo "---UPDATING REPO ON DEV SERVER---" echo "---UPDATING REPO ON DEV SERVER---"

View File

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

View File

@ -73,11 +73,11 @@ msgstr ""
msgid "A 3D globe of our whole map" msgid "A 3D globe of our whole map"
msgstr "" msgstr ""
#: src/ssr-components/Main.jsx:72 #: src/ssr-components/Main.jsx:73
msgid "PixelPlanet.fun" msgid "PixelPlanet.fun"
msgstr "" 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" msgid "Place color pixels on an map styled canvas with other players online"
msgstr "" msgstr ""
@ -270,39 +270,28 @@ msgstr ""
msgid "Password must be shorter than 60 characters." msgid "Password must be shorter than 60 characters."
msgstr "" msgstr ""
#: src/utils/validation.js:74 #: src/routes/api/captcha.js:22
msgid "Could not connect to server, please try again later :(" msgid "No captcha text given"
msgstr "" msgstr ""
#: src/utils/validation.js:80 #: src/routes/api/captcha.js:36
msgid "I think we experienced some error :(" msgid "You took too long, try again."
msgstr "" msgstr ""
#: src/routes/api/auth/change_mail.js:41 #: src/routes/api/captcha.js:42
#: src/routes/api/auth/change_passwd.js:37 msgid "You failed your captcha"
#: src/routes/api/auth/delete_account.js:38
msgid "You are not authenticated."
msgstr "" msgstr ""
#: src/routes/api/auth/change_mail.js:50 #: src/routes/api/captcha.js:48
#: src/routes/api/auth/change_passwd.js:46 msgid "Unknown Captcha Error"
#: src/routes/api/auth/delete_account.js:48
msgid "Incorrect password!"
msgstr "" msgstr ""
#: src/routes/api/auth/verify.js:25 #: src/routes/api/captcha.js:55
#: src/routes/api/auth/verify.js:32 msgid "Server error occured"
msgid "Mail verification"
msgstr "" msgstr ""
#: src/routes/api/auth/verify.js:26 #: src/routes/api/auth/logout.js:13
msgid "You are now verified :)" msgid "You are not even logged in."
msgstr ""
#: src/routes/api/auth/verify.js:32
msgid ""
"Your mail verification code is invalid or already expired :(, please "
"request a new one."
msgstr "" msgstr ""
#: src/routes/api/auth/register.js:31 #: src/routes/api/auth/register.js:31
@ -321,8 +310,31 @@ msgstr ""
msgid "Failed to establish session after register :(" msgid "Failed to establish session after register :("
msgstr "" msgstr ""
#: src/routes/api/auth/logout.js:13 #: src/routes/api/auth/verify.js:25
msgid "You are not even logged in." #: 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 "" msgstr ""
#: src/ssr-components/RedirectionPage.jsx:20 #: 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", "main": "server.js",
"scripts": { "scripts": {
"build": "babel-node scripts/run prebuild && npm run webpack", "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", "clean": "babel-node scripts/run clean",
"webpack": "webpack --config ./webpack.config.web.babel.js && parallel-webpack --config ./webpack.config.client.babel.js", "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", "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" "not IE_Mob 11"
], ],
"dependencies": { "dependencies": {
"bcrypt": "^5.0.0", "bcrypt": "^5.0.1",
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"body-parser": "^1.17.2", "body-parser": "^1.17.2",
"bufferutil": "^4.0.3", "bufferutil": "^4.0.3",
"compression": "^1.7.3", "compression": "^1.7.3",
"connect-redis": "^5.0.0", "connect-redis": "^5.1.0",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"core-js": "^3.8.3", "core-js": "^3.9.1",
"cors": "^2.8.4", "cors": "^2.8.4",
"etag": "^1.8.1", "etag": "^1.8.1",
"express": "^4.15.3", "express": "^4.15.3",
@ -50,7 +51,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.1", "multer": "^1.4.1",
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
"nodemailer": "^6.4.17", "nodemailer": "^6.5.0",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-discord": "^0.1.4", "passport-discord": "^0.1.4",
"passport-facebook": "^3.0.0", "passport-facebook": "^3.0.0",
@ -58,10 +59,10 @@
"passport-json": "^1.2.0", "passport-json": "^1.2.0",
"passport-reddit": "^0.2.4", "passport-reddit": "^0.2.4",
"passport-vkontakte": "^0.5.0", "passport-vkontakte": "^0.5.0",
"ppfun-captcha": "^1.6.4",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-icons": "^4.1.0", "react-icons": "^4.2.0",
"react-modal": "^3.12.1",
"react-redux": "^7.2.1", "react-redux": "^7.2.1",
"react-responsive": "^8.2.0", "react-responsive": "^8.2.0",
"react-stay-scrolled": "^7.3.0", "react-stay-scrolled": "^7.3.0",
@ -72,59 +73,58 @@
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"sequelize": "^6.5.0", "sequelize": "^6.5.1",
"sharp": "^0.27.1", "sharp": "^0.27.2",
"startaudiocontext": "^1.2.1", "startaudiocontext": "^1.2.1",
"sweetalert2": "^10.14.0",
"three": "^0.125.2", "three": "^0.125.2",
"three-trackballcontrols": "^0.9.0", "three-trackballcontrols": "^0.9.0",
"ttag": "^1.7.24", "ttag": "^1.7.24",
"ttag-po-loader": "0.0.2", "ttag-po-loader": "0.0.2",
"url-search-params-polyfill": "^8.1.0", "url-search-params-polyfill": "^8.1.0",
"winston": "^3.3.3", "winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.0", "winston-daily-rotate-file": "^4.5.1",
"ws": "^7.4.2" "ws": "^7.4.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.10", "@babel/core": "^7.13.10",
"@babel/node": "^7.12.10", "@babel/node": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.12.12", "@babel/plugin-proposal-decorators": "^7.13.5",
"@babel/plugin-proposal-do-expressions": "^7.10.4", "@babel/plugin-proposal-do-expressions": "^7.12.13",
"@babel/plugin-proposal-export-default-from": "^7.10.4", "@babel/plugin-proposal-export-default-from": "^7.12.13",
"@babel/plugin-proposal-export-namespace-from": "^7.10.4", "@babel/plugin-proposal-export-namespace-from": "^7.12.13",
"@babel/plugin-proposal-function-bind": "^7.11.5", "@babel/plugin-proposal-function-bind": "^7.12.13",
"@babel/plugin-proposal-function-sent": "^7.10.4", "@babel/plugin-proposal-function-sent": "^7.12.13",
"@babel/plugin-proposal-json-strings": "^7.10.4", "@babel/plugin-proposal-json-strings": "^7.13.8",
"@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
"@babel/plugin-proposal-numeric-separator": "^7.12.7", "@babel/plugin-proposal-numeric-separator": "^7.12.13",
"@babel/plugin-proposal-object-rest-spread": "^7.11.0", "@babel/plugin-proposal-object-rest-spread": "^7.13.8",
"@babel/plugin-proposal-optional-chaining": "^7.12.7", "@babel/plugin-proposal-optional-chaining": "^7.13.8",
"@babel/plugin-proposal-pipeline-operator": "^7.10.5", "@babel/plugin-proposal-pipeline-operator": "^7.12.13",
"@babel/plugin-proposal-throw-expressions": "^7.10.4", "@babel/plugin-proposal-throw-expressions": "^7.12.13",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-transform-flow-strip-types": "^7.12.10", "@babel/plugin-transform-flow-strip-types": "^7.13.0",
"@babel/plugin-transform-react-constant-elements": "^7.10.4", "@babel/plugin-transform-react-constant-elements": "^7.13.10",
"@babel/plugin-transform-react-inline-elements": "^7.10.4", "@babel/plugin-transform-react-inline-elements": "^7.12.13",
"@babel/polyfill": "^7.11.5", "@babel/polyfill": "^7.11.5",
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.13.10",
"@babel/preset-flow": "^7.10.4", "@babel/preset-flow": "^7.12.13",
"@babel/preset-react": "^7.12.10", "@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.12.7", "@babel/preset-typescript": "^7.13.0",
"assets-webpack-plugin": "^7.0.0", "assets-webpack-plugin": "^7.0.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"babel-plugin-transform-react-pure-class-to-function": "^1.0.1", "babel-plugin-transform-react-pure-class-to-function": "^1.0.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-plugin-ttag": "^1.7.30", "babel-plugin-ttag": "^1.7.30",
"clean-css": "^5.0.1", "clean-css": "^5.1.1",
"css-loader": "^5.0.1", "css-loader": "^5.1.3",
"eslint": "^7.19.0", "eslint": "^7.22.0",
"eslint-config-airbnb": "^18.2.1", "eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-base": "^14.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-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.22.0", "eslint-plugin-react": "^7.22.0",
@ -140,9 +140,9 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"ttag-cli": "^1.9.1", "ttag-cli": "^1.9.1",
"webpack": "^5.19.0", "webpack": "^5.26.3",
"webpack-bundle-analyzer": "^4.4.0", "webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.4.0", "webpack-cli": "^4.5.0",
"webpack-dev-middleware": "^4.1.0", "webpack-dev-middleware": "^4.1.0",
"webpack-hot-middleware": "^2.18.0", "webpack-hot-middleware": "^2.18.0",
"webpack-node-externals": "^2.5.2", "webpack-node-externals": "^2.5.2",

View File

@ -92,6 +92,10 @@ async function copy() {
`${deploydir}/example-ecosystem-backup.yml`, `${deploydir}/example-ecosystem-backup.yml`,
`${builddir}/ecosystem-backup.example.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 * Collect api fetch commands for actions here
* (chunk and tiles requests in ui/ChunkLoader*.js) * (chunk and tiles requests in ui/ChunkLoader*.js)
* (user settings requests in their components)
* *
* @flow * @flow
*/ */
import { t } from 'ttag';
/* /*
* Adds customizeable timeout to fetch * Adds customizeable timeout to fetch
@ -27,123 +27,226 @@ async function fetchWithTimeout(resource, options) {
} }
/* /*
* block / unblock user * Parse response from API
* userId id of user to block * @param response
* block true if block, false if unblock * @return Object of response
* return error string or null if successful
*/ */
export async function requestBlock(userId: number, block: boolean) { async function parseAPIresponse(response) {
const response = await fetchWithTimeout('api/block', { const { status: code } = response;
method: 'POST',
credentials: 'include', if (code === 429) {
headers: { let error = t`You made too many requests`;
'Content-Type': 'application/json', const retryAfter = response.headers.get('Retry-After');
}, if (!Number.isNaN(Number(retryAfter))) {
body: JSON.stringify({ const ti = Math.floor(retryAfter / 60);
userId, error += `, ${t`try again after ${ti}min`}`;
block, }
}), return {
}); errors: [error],
};
}
try { try {
const res = await response.json(); return await response.json();
if (res.errors) { } catch (e) {
return res.errors[0]; return {
} errors: [t`Connection error ${code} :(`],
if (response.ok && res.status === 'ok') { };
return null;
}
return 'Unknown Error';
} catch {
return 'Connection Error';
} }
} }
/*
* 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 * start new DM channel with user
* query Object with either userId: number or userName: string * @param query Object with either userId: number or userName: string
* return channel Array on success, error string if not * @return channel Array on success, error string if not
*/ */
export async function requestStartDm(query) { export async function requestStartDm(query) {
const response = await fetchWithTimeout('api/startdm', { const res = await makeAPIPOSTRequest(
method: 'POST', 'api/startdm',
credentials: 'include', query,
headers: { );
'Content-Type': 'application/json', if (res.errors) {
}, return res.errors[0];
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';
} }
if (res.channel) {
return res.channel;
}
return t`Unknown Error`;
} }
/* /*
* set receiving of all DMs on/off * set receiving of all DMs on/off
* block true if blocking all dms, false if unblocking * @param block true if blocking all dms, false if unblocking
* return error string or null if successful * @return error string or null if successful
*/ */
export async function requestBlockDm(block: boolean) { export async function requestBlockDm(block: boolean) {
const response = await fetchWithTimeout('api/blockdm', { const res = await makeAPIPOSTRequest(
method: 'POST', 'api/blockdm',
credentials: 'include', { block },
headers: { );
'Content-Type': 'application/json', if (res.errors) {
}, return res.errors[0];
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';
} }
if (res.status === 'ok') {
return null;
}
return t`Unknown Error`;
} }
/* /*
* leaving Chat Channel (i.e. DM channel) * leaving Chat Channel (i.e. DM channel)
* channelId 8nteger id of channel * @param channelId 8nteger id of channel
* return error string or null if successful * @return error string or null if successful
*/ */
export async function requestLeaveChan(channelId: boolean) { export async function requestLeaveChan(channelId: boolean) {
const response = await fetchWithTimeout('api/leavechan', { const res = await makeAPIPOSTRequest(
method: 'POST', 'api/leavechan',
credentials: 'include', { channelId },
headers: { );
'Content-Type': 'application/json', if (res.errors) {
}, return res.errors[0];
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';
} }
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 { export function toggleChatBox(): Action {
return { return {
type: 'TOGGLE_CHAT_BOX', 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 { return {
type: 'SET_PLACE_ALLOWED', type: 'SET_REQUESTING_PIXEL',
requestingPixel, requestingPixel,
}; };
} }

View File

@ -14,6 +14,7 @@ export type Action =
icon: string, icon: string,
confirmButtonText: string, confirmButtonText: string,
} }
| { type: 'CLOSE_ALERT' }
| { type: 'TOGGLE_GRID' } | { type: 'TOGGLE_GRID' }
| { type: 'TOGGLE_PIXEL_NOTIFY' } | { type: 'TOGGLE_PIXEL_NOTIFY' }
| { type: 'TOGGLE_AUTO_ZOOM_IN' } | { type: 'TOGGLE_AUTO_ZOOM_IN' }
@ -28,7 +29,7 @@ export type Action =
| { type: 'SELECT_STYLE', style: string } | { type: 'SELECT_STYLE', style: string }
| { type: 'SET_NOTIFICATION', notification: string } | { type: 'SET_NOTIFICATION', notification: string }
| { type: 'UNSET_NOTIFICATION' } | { type: 'UNSET_NOTIFICATION' }
| { type: 'SET_PLACE_ALLOWED', requestingPixel: boolean } | { type: 'SET_REQUESTING_PIXEL', requestingPixel: boolean }
| { type: 'SET_HOVER', hover: Cell } | { type: 'SET_HOVER', hover: Cell }
| { type: 'UNSET_HOVER' } | { type: 'UNSET_HOVER' }
| { type: 'SET_WAIT', wait: ?number } | { 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 React from 'react';
import { t } from 'ttag'; import { t } from 'ttag';
import { import {
validateEMail, validatePassword, parseAPIresponse, validateEMail, validatePassword,
} from '../utils/validation'; } from '../utils/validation';
import { requestMailChange } from '../actions/fetch';
function validate(email, password) { function validate(email, password) {
const errors = []; const errors = [];
@ -20,23 +21,6 @@ function validate(email, password) {
return errors; 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 { class ChangeMail extends React.Component {
constructor() { constructor() {
super(); super();
@ -64,7 +48,7 @@ class ChangeMail extends React.Component {
if (errors.length > 0) return; if (errors.length > 0) return;
this.setState({ submitting: true }); this.setState({ submitting: true });
const { errors: resperrors } = await submitMailchange(email, password); const { errors: resperrors } = await requestMailChange(email, password);
if (resperrors) { if (resperrors) {
this.setState({ this.setState({
errors: resperrors, errors: resperrors,

View File

@ -5,7 +5,8 @@
import React from 'react'; import React from 'react';
import { t } from 'ttag'; import { t } from 'ttag';
import { validateName, parseAPIresponse } from '../utils/validation'; import { validateName } from '../utils/validation';
import { requestNameChange } from '../actions/fetch';
function validate(name) { function validate(name) {
@ -17,22 +18,6 @@ function validate(name) {
return errors; 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 { class ChangeName extends React.Component {
constructor() { constructor() {
super(); super();
@ -58,7 +43,7 @@ class ChangeName extends React.Component {
if (errors.length > 0) return; if (errors.length > 0) return;
this.setState({ submitting: true }); this.setState({ submitting: true });
const { errors: resperrors } = await submitNamechange(name); const { errors: resperrors } = await requestNameChange(name);
if (resperrors) { if (resperrors) {
this.setState({ this.setState({
errors: resperrors, errors: resperrors,

View File

@ -3,9 +3,10 @@
* @flow * @flow
*/ */
import React from 'react'; import React, { useState } from 'react';
import { t } from 'ttag'; 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) { function validate(mailreg, password, newPassword, confirmPassword) {
const errors = []; const errors = [];
@ -24,136 +25,92 @@ function validate(mailreg, password, newPassword, confirmPassword) {
return errors; return errors;
} }
async function submitPasswordChange(newPassword, password) { const ChangePassword = ({ mailreg, done, cancel }) => {
const body = JSON.stringify({ const [password, setPassword] = useState('');
password, const [newPassword, setNewPassword] = useState('');
newPassword, const [confirmPassword, setConfirmPassword] = useState('');
}); const [success, setSuccess] = useState(false);
const response = await fetch('./api/auth/change_passwd', { const [submitting, setSubmitting] = useState(false);
method: 'POST', const [errors, setErrors] = useState([]);
headers: {
'Content-type': 'application/json',
},
body,
credentials: 'include',
});
return parseAPIresponse(response); if (success) {
}
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;
return ( return (
<div className="inarea"> <div className="inarea">
<form onSubmit={this.handleSubmit}> <p className="modalmessage">{t`Changed Password successfully.`}</p>
{errors.map((error) => ( <button type="button" onClick={done}>Close</button>
<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>
</div> </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 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 useWindowSize from '../utils/reactHookResize';
import { showChatModal } from '../actions'; import { showChatModal } from '../actions';
import Chat from './Chat'; import Chat from './Chat';
function ChatBox({ const ChatBox = () => {
chatOpen,
triggerModal,
}) {
const [render, setRender] = useState(false); const [render, setRender] = useState(false);
const chatOpen = useSelector((state) => state.modal.chatOpen);
const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
window.setTimeout(() => { window.setTimeout(() => {
if (chatOpen) setRender(true); if (chatOpen) setRender(true);
@ -31,7 +31,7 @@ function ChatBox({
const { width } = useWindowSize(); const { width } = useWindowSize();
if (width < 604 && chatOpen) { if (width < 604 && chatOpen) {
triggerModal(); dispatch(showChatModal(true));
} }
return ( return (
@ -44,19 +44,6 @@ function ChatBox({
</div> </div>
) )
); );
} };
function mapStateToProps(state: State) { export default React.memo(ChatBox);
const { chatOpen } = state.modal;
return { chatOpen };
}
function mapDispatchToProps(dispatch) {
return {
triggerModal() {
dispatch(showChatModal(true));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatBox);

View File

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

View File

@ -7,7 +7,8 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { t } from 'ttag'; import { t } from 'ttag';
import { validatePassword, parseAPIresponse } from '../utils/validation'; import { validatePassword } from '../utils/validation';
import { requestDeleteAccount } from '../actions/fetch';
import { logoutUser } from '../actions'; import { logoutUser } from '../actions';
function validate(password) { function validate(password) {
@ -19,22 +20,6 @@ function validate(password) {
return errors; 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 { class DeleteAccount extends React.Component {
constructor() { constructor() {
super(); super();
@ -60,7 +45,7 @@ class DeleteAccount extends React.Component {
if (errors.length > 0) return; if (errors.length > 0) return;
this.setState({ submitting: true }); this.setState({ submitting: true });
const { errors: resperrors } = await submitDeleteAccount(password); const { errors: resperrors } = await requestDeleteAccount(password);
if (resperrors) { if (resperrors) {
this.setState({ this.setState({
errors: resperrors, errors: resperrors,

View File

@ -31,11 +31,6 @@ const HelpModal = () => {
const bindShift = <kbd> {c('keybinds').t`Shift`}</kbd>; const bindShift = <kbd> {c('keybinds').t`Shift`}</kbd>;
const bindC = <kbd>{c('keybinds').t`C`}</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 guildedLink = <a href="https://pixelplanet.fun/guilded">guilded</a>;
const getIPLink = <a href="https://www.whatismyip.com/">{t`your IP`}</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>; 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 /> {jt`Click ${mouseSymbol} middle mouse button or ${touchSymbol} long-tap to select current hovering color`}<br />
</div> </div>
<p>{t`Partners:`} <a href="https://www.crazygames.com/c/io" target="_blank" rel="noopener noreferrer">crazygames.com</a></p> <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> </p>
); );
}; };

View File

@ -7,8 +7,9 @@ import { connect } from 'react-redux';
import { t } from 'ttag'; import { t } from 'ttag';
import { import {
validateEMail, validateName, validatePassword, parseAPIresponse, validateEMail, validateName, validatePassword,
} from '../utils/validation'; } from '../utils/validation';
import { requestLogin } from '../actions/fetch';
import { loginUser } from '../actions'; import { loginUser } from '../actions';
@ -24,22 +25,6 @@ function validate(nameoremail, password) {
return errors; 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 = { const inputStyles = {
display: 'inline-block', display: 'inline-block',
width: '100%', width: '100%',
@ -73,7 +58,7 @@ class LogInForm extends React.Component {
if (errors.length > 0) return; if (errors.length > 0) return;
this.setState({ submitting: true }); this.setState({ submitting: true });
const { errors: resperrors, me } = await submitLogin( const { errors: resperrors, me } = await requestLogin(
nameoremail, nameoremail,
password, password,
); );

View File

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

View File

@ -4,7 +4,8 @@
*/ */
import React from 'react'; import React from 'react';
import { t } from 'ttag'; import { t } from 'ttag';
import { validateEMail, parseAPIresponse } from '../utils/validation'; import { validateEMail } from '../utils/validation';
import { requestNewPassword } from '../actions/fetch';
function validate(email) { function validate(email) {
const errors = []; const errors = [];
@ -13,21 +14,6 @@ function validate(email) {
return errors; 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 = { const inputStyles = {
display: 'inline-block', display: 'inline-block',
width: '100%', width: '100%',
@ -60,7 +46,7 @@ class NewPasswordForm extends React.Component {
if (errors.length > 0) return; if (errors.length > 0) return;
this.setState({ submitting: true }); this.setState({ submitting: true });
const { errors: resperrors } = await submitNewpass(email); const { errors: resperrors } = await requestNewPassword(email);
if (resperrors) { if (resperrors) {
this.setState({ this.setState({
errors: resperrors, errors: resperrors,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -183,25 +183,6 @@ class UserArea extends React.Component {
done={() => { this.setState({ socialSettingsExtended: false }); }} 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> </p>
); );
} }
@ -212,11 +193,13 @@ function mapStateToProps(state: State) {
const { const {
name, name,
mailreg, mailreg,
} = state.user;
const {
totalPixels, totalPixels,
dailyTotalPixels, dailyTotalPixels,
ranking, ranking,
dailyRanking, dailyRanking,
} = state.user; } = state.ranks;
const stats = { const stats = {
totalPixels, totalPixels,
dailyTotalPixels, dailyTotalPixels,

View File

@ -6,8 +6,8 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { t } from 'ttag'; import { t } from 'ttag';
import { parseAPIresponse } from '../utils/validation';
import { setMinecraftName, remFromMessages } from '../actions'; import { setMinecraftName, remFromMessages } from '../actions';
import { requestResendVerify, requestMcLink } from '../actions/fetch';
class UserMessages extends React.Component { class UserMessages extends React.Component {
@ -31,11 +31,8 @@ class UserMessages extends React.Component {
resentVerify: true, resentVerify: true,
}); });
const response = await fetch('./api/auth/resend_verify', { const { errors } = await requestResendVerify();
credentials: 'include',
});
const { errors } = await parseAPIresponse(response);
const verifyAnswer = (errors) const verifyAnswer = (errors)
? errors[0] ? errors[0]
: t`A new verification mail is getting sent to you.`; : t`A new verification mail is getting sent to you.`;
@ -50,15 +47,9 @@ class UserMessages extends React.Component {
this.setState({ this.setState({
sentLink: true, 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) { if (errors) {
this.setState({ this.setState({
linkAnswer: errors[0], linkAnswer: errors[0],

View File

@ -10,6 +10,7 @@ if (process.env.BROWSER) {
} }
export const PORT = process.env.PORT || 80; 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_USER = process.env.GMAIL_USER || null;
export const GMAIL_PW = process.env.GMAIL_PW || 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 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 USE_XREALIP = process.env.USE_XREALIP || false;
export const BACKUP_URL = process.env.BACKUP_URL || null; 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'; export const MYSQL_PW = process.env.MYSQL_PW || 'password';
// Social // Social
export const DISCORD_INVITE = process.env.DISCORD_INVITE
|| 'https://discordapp.com/';
export const GUILDED_INVITE = process.env.GUILDED_INVITE export const GUILDED_INVITE = process.env.GUILDED_INVITE
|| 'https://www.guilded.gg/'; || '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 // time on which to display captcha in minutes
export const CAPTCHA_TIME = parseInt(process.env.CAPTCHA_TIME, 10) || 30; 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'; 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 gui from './gui';
import modal from './modal'; import modal from './modal';
import user from './user'; import user from './user';
import ranks from './ranks';
import alert from './alert';
import chat from './chat'; import chat from './chat';
import contextMenu from './contextMenu'; import contextMenu from './contextMenu';
import chatRead from './chatRead'; import chatRead from './chatRead';
@ -17,6 +19,8 @@ import type { CanvasState } from './canvas';
import type { GUIState } from './gui'; import type { GUIState } from './gui';
import type { ModalState } from './modal'; import type { ModalState } from './modal';
import type { UserState } from './user'; import type { UserState } from './user';
import type { RanksState } from './ranks';
import type { AlertState } from './alert';
import type { ChatState } from './chat'; import type { ChatState } from './chat';
import type { ContextMenuState } from './contextMenu'; import type { ContextMenuState } from './contextMenu';
import type { FetchingState } from './fetching'; import type { FetchingState } from './fetching';
@ -27,6 +31,8 @@ export type State = {
gui: GUIState, gui: GUIState,
modal: ModalState, modal: ModalState,
user: UserState, user: UserState,
ranks: RanksState,
alert: AlertState,
chat: ChatState, chat: ChatState,
contextMenu: ContextMenuState, contextMenu: ContextMenuState,
chatRead: ChatReadState, chatRead: ChatReadState,
@ -38,7 +44,9 @@ const config = {
storage: localForage, storage: localForage,
blacklist: [ blacklist: [
'user', 'user',
'ranks',
'canvas', 'canvas',
'alert',
'modal', 'modal',
'chat', 'chat',
'contextMenu', 'contextMenu',
@ -52,6 +60,8 @@ export default persistCombineReducers(config, {
gui, gui,
modal, modal,
user, user,
ranks,
alert,
chat, chat,
contextMenu, contextMenu,
chatRead, 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'; import { createNameRegExp } from '../core/utils';
export type UserState = { export type UserState = {
name: string, name: string,
center: Cell, center: Cell,
@ -13,18 +12,9 @@ export type UserState = {
coolDown: ?number, // ms coolDown: ?number, // ms
lastCoolDownEnd: ?Date, lastCoolDownEnd: ?Date,
requestingPixel: boolean, requestingPixel: boolean,
online: ?number,
// messages are sent by api/me, like not_verified status // messages are sent by api/me, like not_verified status
messages: Array, messages: Array,
mailreg: boolean, mailreg: boolean,
// stats
totalPixels: number,
dailyTotalPixels: number,
ranking: number,
dailyRanking: number,
// global stats
totalRanking: Object,
totalDailyRanking: Object,
// minecraft // minecraft
minecraftname: string, minecraftname: string,
// blocking all Dms // blocking all Dms
@ -46,11 +36,8 @@ const initialState: UserState = {
coolDown: null, coolDown: null,
lastCoolDownEnd: null, lastCoolDownEnd: null,
requestingPixel: true, requestingPixel: true,
online: null,
messages: [], messages: [],
mailreg: false, mailreg: false,
totalRanking: {},
totalDailyRanking: {},
minecraftname: null, minecraftname: null,
blockDm: false, blockDm: false,
isOnMobile: false, isOnMobile: false,
@ -81,7 +68,7 @@ export default function user(
}; };
} }
case 'SET_PLACE_ALLOWED': { case 'SET_REQUESTING_PIXEL': {
const { requestingPixel } = action; const { requestingPixel } = action;
return { return {
...state, ...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 'RECEIVE_ME':
case 'LOGIN': { case 'LOGIN': {
const { const {
name, name,
mailreg, mailreg,
totalPixels,
dailyTotalPixels,
ranking,
dailyRanking,
minecraftname, minecraftname,
blockDm, blockDm,
userlvl, userlvl,
@ -160,10 +123,6 @@ export default function user(
name, name,
messages, messages,
mailreg, mailreg,
totalPixels,
dailyTotalPixels,
ranking,
dailyRanking,
minecraftname, minecraftname,
blockDm, blockDm,
userlvl, userlvl,
@ -184,15 +143,6 @@ export default function user(
}; };
} }
case 'RECEIVE_STATS': {
const { totalRanking, totalDailyRanking } = action;
return {
...state,
totalRanking,
totalDailyRanking,
};
}
case 'SET_NAME': { case 'SET_NAME': {
const { name } = action; const { name } = action;
const nameRegExp = createNameRegExp(name); const nameRegExp = createNameRegExp(name);

View File

@ -8,42 +8,51 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import logger from '../../core/logger'; import logger from '../../core/logger';
import { verifyCaptcha } from '../../utils/captcha'; import { checkCaptchaSolution } from '../../utils/captcha';
import { getIPFromRequest } from '../../utils/ip'; import { getIPFromRequest } from '../../utils/ip';
export default async (req: Request, res: Response) => { export default async (req: Request, res: Response) => {
const ip = getIPFromRequest(req); const ip = getIPFromRequest(req);
const { t } = req.ttag;
try { try {
const { token } = req.body; const { text } = req.body;
if (!token) { if (!text) {
res.status(400) res.status(400)
.json({ errors: [{ msg: 'No token given' }] }); .json({ errors: [t`No captcha text given`] });
return; return;
} }
if (!await verifyCaptcha(token, ip)) { const ret = await checkCaptchaSolution(text, ip);
logger.info(`CAPTCHA ${ip} failed his captcha`);
res.status(422)
.json({
errors: [{
msg:
'You failed your captcha',
}],
});
return;
}
res.status(200) switch (ret) {
.json({ success: true }); 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) { } catch (error) {
logger.error('checkHuman', error); logger.error('CAPTCHA', error);
res.status(500) res.status(500)
.json({ .json({
errors: [{ errors: [t`Server error occured`],
msg:
'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 // captcah doesn't need a user
router.post('/captcha', captcha); router.post('/captcha', captcha);
@ -89,11 +94,6 @@ router.post('/block', block);
router.post('/blockdm', blockdm); router.post('/blockdm', blockdm);
/*
* make localisations available
*/
router.use(expressTTag);
router.get('/chathistory', chatHistory); router.get('/chathistory', chatHistory);
router.get('/me', me); router.get('/me', me);

View File

@ -11,7 +11,6 @@
/* eslint-disable max-len */ /* eslint-disable max-len */
import React from 'react'; import React from 'react';
import { CAPTCHA_METHOD, CAPTCHA_SITEKEY } from '../core/config';
const Html = ({ const Html = ({
title, title,
@ -25,8 +24,6 @@ const Html = ({
styles, styles,
// code as string // code as string
code, code,
// if recaptcha should get loaded
useCaptcha,
}) => ( }) => (
<html className="no-js" lang="en"> <html className="no-js" lang="en">
<head> <head>
@ -48,10 +45,6 @@ const Html = ({
dangerouslySetInnerHTML={{ __html: style.cssText }} 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 && ( {code && (
<script <script
// eslint-disable-next-line react/no-danger // eslint-disable-next-line react/no-danger
@ -67,24 +60,6 @@ const Html = ({
{body} {body}
</div> </div>
{scripts && scripts.map((script) => <script key={script} src={script} />)} {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> </body>
</html> </html>
); );

View File

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

View File

@ -5,7 +5,6 @@ import thunk from 'redux-thunk';
import { persistStore } from 'redux-persist'; import { persistStore } from 'redux-persist';
import audio from './audio'; import audio from './audio';
import swal from './sweetAlert';
import protocolClientHook from './protocolClientHook'; import protocolClientHook from './protocolClientHook';
import rendererHook from './rendererHook'; import rendererHook from './rendererHook';
// import ads from './ads'; // import ads from './ads';
@ -13,6 +12,7 @@ import array from './array';
import promise from './promise'; import promise from './promise';
import notifications from './notifications'; import notifications from './notifications';
import title from './title'; import title from './title';
import placePixelControl from './placePixelControl';
import extensions from './extensions'; import extensions from './extensions';
import reducers from '../reducers'; import reducers from '../reducers';
@ -25,12 +25,12 @@ const store = createStore(
thunk, thunk,
promise, promise,
array, array,
swal,
audio, audio,
notifications, notifications,
title, title,
protocolClientHook, protocolClientHook,
rendererHook, rendererHook,
placePixelControl,
extensions, 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; break;
} }
case 'SET_PLACE_ALLOWED': { case 'SET_REQUESTING_PIXEL': {
const renderer = getRenderer(); const renderer = getRenderer();
renderer.forceNextSubRender = true; renderer.forceNextSubRender = true;
break; 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; border: 1px solid #dddddd;
text-align: left; text-align: left;
padding: 8px; padding: 8px;
max-width: 18em;
overflow-x: hidden;
} }
tr:nth-child(even) { tr:nth-child(even) {
@ -322,7 +324,7 @@ tr:nth-child(even) {
padding: 0; padding: 0;
} }
.Modal { .Modal, .Alert {
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
@ -334,14 +336,26 @@ tr:nth-child(even) {
border-radius: 4px; border-radius: 4px;
outline: currentcolor none medium; outline: currentcolor none medium;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
transition: opacity 200ms ease-in-out;
opacity: 0;
}
.Modal {
height: 80%; height: 80%;
max-height: 900px;
width: 70%; 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 { .modaltext, .modalcotext {
color: hsla(218, 5%, 47%, .6); color: hsla(0, 0%, 0%, 0.6);
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
line-height: normal; line-height: normal;
@ -370,7 +384,7 @@ tr:nth-child(even) {
.modaldesc { .modaldesc {
box-sizing: border-box; box-sizing: border-box;
flex: 1 1 auto; flex: 1 1 auto;
color: hsla(218, 5%, 47%, .6); color: hsla(0, 0%, 0%, 0.6);
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
font-weight: 500; font-weight: 500;
@ -397,7 +411,7 @@ tr:nth-child(even) {
} }
.modalcvtext { .modalcvtext {
color: hsla(218, 5%, 47%, .6); color: hsla(0, 0%, 0%, 0.6);
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
padding: 0; padding: 0;
@ -432,7 +446,7 @@ tr:nth-child(even) {
} }
@media (max-width: 604px) { @media (max-width: 604px) {
.Modal { .Modal, .Alert {
position: fixed; position: fixed;
top: 0px; top: 0px;
left: 0px; left: 0px;
@ -446,16 +460,26 @@ tr:nth-child(even) {
padding: 5%; padding: 5%;
} }
} }
.Overlay {
.OverlayModal, .OverlayAlert {
position: fixed; position: fixed;
top: 0px; top: 0px;
left: 0px; left: 0px;
right: 0px; right: 0px;
bottom: 0px; bottom: 0px;
background-color: rgba(255, 255, 255, 0.75); background-color: rgba(255, 255, 255, 0.75);
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.OverlayModal {
z-index: 4; z-index: 4;
} }
.OverlayAlert {
z-index: 6;
}
.chatbox div .chatarea { .chatbox div .chatarea {
height: 174px; height: 174px;
} }
@ -697,15 +721,6 @@ tr:nth-child(even) {
visibility: hidden; visibility: hidden;
} }
.ReactModal__Overlay { .Modal.show, .Alert.show, .OverlayAlert.show, .OverlayModal.show {
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.ReactModal__Overlay--after-open{
opacity: 1; opacity: 1;
} }
.ReactModal__Overlay--before-close{
opacity: 0;
}

View File

@ -11,7 +11,7 @@ tr:nth-child(odd) {
border-radius: 10px; border-radius: 10px;
} }
.Overlay { .OverlayModal, .OverlayAlert {
background: linear-gradient(175deg, #61dceaab , #ffb1e1, #ecffec, #ffb1e1, #61dceaab); background: linear-gradient(175deg, #61dceaab , #ffb1e1, #ecffec, #ffb1e1, #61dceaab);
} }
@ -54,3 +54,8 @@ tr:nth-child(odd) {
background-size: cover; background-size: cover;
border-radius: 10px; 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; background-color: #15374fd1;
} }
.Modal { .Modal, .Alert {
background: #444242 none repeat scroll 0 0;; background: #444242 none repeat scroll 0 0;;
color: #f4f4f4; color: #f4f4f4;
}
.Modal {
border-radius: 21px; border-radius: 21px;
} }
.Alert {
border-radius: 12px;
}
.modaltext, .modalcotext { .modaltext, .modalcotext {
color: #f4f4f4; color: #f4f4f4;
} }
@ -104,7 +111,7 @@ tr:nth-child(even) {
background-color: #6f6f75; background-color: #6f6f75;
} }
.Overlay { .OverlayModal, .OverlayAlert {
background-color: rgba(187, 187, 187, 0.75); 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; background-color: #15374fd1;
} }
.Modal { .Modal, .Alert {
background: #444242 none repeat scroll 0 0;; background: #444242 none repeat scroll 0 0;;
color: #f4f4f4; color: #f4f4f4;
} }
@ -96,7 +96,7 @@ tr:nth-child(even) {
background-color: #6f6f75; background-color: #6f6f75;
} }
.Overlay { .OverlayModal, .OverlayAlert {
background-color: rgba(187, 187, 187, 0.75); background-color: rgba(187, 187, 187, 0.75);
} }

View File

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

View File

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

View File

@ -3,117 +3,70 @@
* @flow * @flow
*/ */
import fetch from 'isomorphic-fetch';
import logger from '../core/logger'; import logger from '../core/logger';
import redis from '../data/redis'; import redis from '../data/redis';
import { getIPv6Subnet } from './ip';
import { import {
CAPTCHA_METHOD, CAPTCHA_URL,
CAPTCHA_SECRET,
CAPTCHA_TIME, CAPTCHA_TIME,
CAPTCHA_TIMEOUT,
} from '../core/config'; } from '../core/config';
const TTL_CACHE = CAPTCHA_TIME * 60; // seconds 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';
/** function captchaTextFilter(text: string) {
* https://stackoverflow.com/questions/27297067/google-recaptcha-how-to-get-user-response-and-validate-in-the-server-side let ret = text.toString('utf8');
* ret = ret.split('l').join('i');
* @param token ret = ret.split('0').join('O');
* @param ip ret = ret.toLowerCase();
* @returns {Promise.<boolean>} return ret;
*/
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;
} }
/* /*
* https://docs.hcaptcha.com/ * set captcha solution
* *
* @param token * @param text Solution of captcha
* @param ip * @param ip
* @return boolean, true if successful, false on error or fail * @param ttl time to be valid in seconds
*/ */
async function verifyHCaptcha( export function setCaptchaSolution(
token: string, text: string,
ip: string, ip: string,
): Promise<boolean> { ) {
const response = await fetch(HCAPTCHA_ENDPOINT, { const key = `capt:${ip}`;
method: 'POST', return redis.setAsync(key, captchaTextFilter(text), 'EX', CAPTCHA_TIMEOUT);
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;
} }
/* /*
* verify captcha token from client * check captcha solution
* *
* @param token token of solved captcha from client * @param text Solution of captcha
* @param ip * @param ip
* @returns Boolean if successful * @return 0 if solution right
* 1 if timed out
* 2 if wrong
*/ */
export async function verifyCaptcha( export async function checkCaptchaSolution(
token: string, text: string,
ip: string, ip: string,
): Promise<boolean> { ) {
try { const ipn = getIPv6Subnet(ip);
if (!CAPTCHA_METHOD) { const key = `capt:${ip}`;
return true; 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}`; logger.info(
`CAPTCHA ${ip} got captcha wrong (${text} instead of ${solution})`,
switch (CAPTCHA_METHOD) { );
case 1: return 2;
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);
} }
return false; logger.info(`CAPTCHA ${ip} timed out`);
return 1;
} }
/* /*
@ -123,11 +76,11 @@ export async function verifyCaptcha(
* @return boolean true if needed * @return boolean true if needed
*/ */
export async function needCaptcha(ip: string) { export async function needCaptcha(ip: string) {
if (!CAPTCHA_METHOD) { if (!CAPTCHA_URL) {
return false; return false;
} }
const key = `human:${ip}`; const key = `human:${getIPv6Subnet(ip)}`;
const ttl: number = await redis.ttlAsync(key); const ttl: number = await redis.ttlAsync(key);
if (ttl > 0) { if (ttl > 0) {
return false; return false;
@ -135,6 +88,3 @@ export async function needCaptcha(ip: string) {
logger.info(`CAPTCHA ${ip} got captcha`); logger.info(`CAPTCHA ${ip} got captcha`);
return true; 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 * @flow
*/ */
import isCloudflareIp from './cloudflareip';
import logger from '../core/logger'; import logger from '../core/logger';
import { USE_XREALIP } from '../core/config'; 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 { export function getHostFromRequest(req): ?string {
const { headers } = req; const { headers } = req;
const host = headers['x-forwarded-host'] || headers.host; const host = headers['x-forwarded-host'] || headers.host;
@ -26,30 +17,25 @@ export function getHostFromRequest(req): ?string {
} }
export function getIPFromRequest(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); let conip = (connection ? connection.remoteAddress : socket.remoteAddress);
conip = conip || '0.0.0.1'; conip = conip || '0.0.0.1';
if (USE_XREALIP) { if (!USE_XREALIP) {
const ip = headers['x-real-ip']; logger.warn(
return ip || conip; `Connection not going through reverse proxy! IP: ${conip}`, req.headers,
);
} }
if (!headers['x-forwarded-for'] || !isTrustedProxy(conip)) { return 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;
} }
export function getIPv6Subnet(ip: string): string { export function getIPv6Subnet(ip: string): string {

View File

@ -61,23 +61,3 @@ export function validatePassword(password) {
} }
return false; 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 generateMainPage from './ssr-components/Main';
import { SECOND, MONTH } from './core/constants'; 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 { ccToCoords } from './utils/location';
import { startAllCanvasLoops } from './core/tileserver'; 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) => { app.use('/guilded', (req, res) => {
res.redirect(GUILDED_INVITE); res.redirect(GUILDED_INVITE);
}); });
@ -203,10 +200,13 @@ const promise = models.sync({ alter: { drop: false } })
// const promise = models.sync() // const promise = models.sync()
.catch((err) => logger.error(err.stack)); .catch((err) => logger.error(err.stack));
promise.then(() => { promise.then(() => {
server.listen(PORT, () => { server.listen(PORT, HOST, () => {
rankings.updateRanking(); rankings.updateRanking();
chatProvider.initialize(); chatProvider.initialize();
const address = server.address(); 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: { entry: {
web: [path.resolve(__dirname, 'src', 'web.js')], web: [path.resolve(__dirname, 'src', 'web.js')],
backup: [path.resolve(__dirname, 'src', 'backup.js')], backup: [path.resolve(__dirname, 'src', 'backup.js')],
captchaserver: [path.resolve(__dirname, 'src', 'captchaserver.js')],
}, },
output: { output: {