Merge branch 'captcha'
This commit is contained in:
commit
e237d74481
|
@ -30,6 +30,7 @@
|
|||
"react/jsx-one-expression-per-line": "off",
|
||||
"react/jsx-closing-tag-location":"off",
|
||||
"jsx-a11y/click-events-have-key-events":"off",
|
||||
"jsx-a11y/no-static-element-interactions":"off",
|
||||
"no-continue": "off",
|
||||
"no-multiple-empty-lines": "off",
|
||||
"lines-between-class-members":["warn", "always",{"exceptAfterSingleLine": true}]
|
||||
|
|
41
README.md
41
README.md
|
@ -69,8 +69,9 @@ Configuration takes place in the environment variables that are defined in ecosy
|
|||
|
||||
| Variable | Description | Example |
|
||||
|----------------|:-------------------------|------------------------:|
|
||||
| PORT | Port | 80 |
|
||||
| REDIS_URL | URL:PORT of redis server | "redis://localhost:6379" |
|
||||
| PORT | Own Port | 80 |
|
||||
| HOST | Own Host | "localhost" |
|
||||
| REDIS_URL | URL:PORT of redis server | "redis://localhost:6379"|
|
||||
| MYSQL_HOST | MySql Host | "localhost" |
|
||||
| MYSQL_USER | MySql User | "user" |
|
||||
| MYSQL_PW | MySql Password | "password" |
|
||||
|
@ -78,24 +79,22 @@ Configuration takes place in the environment variables that are defined in ecosy
|
|||
|
||||
#### Optional Configuration
|
||||
|
||||
| Variable | Description | Example |
|
||||
|-------------------|:--------------------------------------|--------------------|
|
||||
| ASSET_SERVER | URL for assets | "http://localhost" |
|
||||
| USE_PROXYCHECK | Check users for Proxies | 0 |
|
||||
| APISOCKET_KEY | Key for API Socket for SpecialAccess™ | "SDfasife3" |
|
||||
| ADMIN_IDS | Ids of users with Admin rights | "1,12,3" |
|
||||
| CAPTCHA_METHOD | 0: none, 1: reCaptcha, 2: hCaptcha | 2 |
|
||||
| CAPTCHA_SECRET | re/hCaptcha secret key | "asdieewff" |
|
||||
| CAPTCHA_SITEKEY | re/hCaptcha site key | "23ksdfssd" |
|
||||
| CAPTCHA_TIME | time in minutes between captchas | 30 |
|
||||
| SESSION_SECRET | random sting for express sessions | "ayylmao" |
|
||||
| LOG_MYSQL | if sql queries should get logged | 0 |
|
||||
| USE_XREALIP | see cloudflare section | 1 |
|
||||
| BACKUP_URL | url of backup server (see Backup) | "http://localhost" |
|
||||
| BACKUP_DIR | mounted directory of backup server | "/mnt/backup/" |
|
||||
| GMAIL_USER | gmail username if used for mails | "ppfun@gmail.com" |
|
||||
| GMAIL_PW | gmail password if used for mails | "lolrofls" |
|
||||
| HOURLY_EVENT | run hourly void event on main canvas | 1 |
|
||||
| Variable | Description | Example |
|
||||
|-------------------|:--------------------------------------|-------------------------|
|
||||
| ASSET_SERVER | URL for assets | "http://localhost" |
|
||||
| USE_PROXYCHECK | Check users for Proxies | 0 |
|
||||
| APISOCKET_KEY | Key for API Socket for SpecialAccess™ | "SDfasife3" |
|
||||
| ADMIN_IDS | Ids of users with Admin rights | "1,12,3" |
|
||||
| CAPTCHA_URL | URL where captcha is served | "http://localhost:8080" |
|
||||
| CAPTCHA_TIME | time in minutes between captchas | 30 |
|
||||
| SESSION_SECRET | random sting for express sessions | "ayylmao" |
|
||||
| LOG_MYSQL | if sql queries should get logged | 0 |
|
||||
| USE_XREALIP | see ngins / CDN section | 1 |
|
||||
| BACKUP_URL | url of backup server (see Backup) | "http://localhost" |
|
||||
| BACKUP_DIR | mounted directory of backup server | "/mnt/backup/" |
|
||||
| GMAIL_USER | gmail username if used for mails | "ppfun@gmail.com" |
|
||||
| GMAIL_PW | gmail password if used for mails | "lolrofls" |
|
||||
| HOURLY_EVENT | run hourly void event on main canvas | 1 |
|
||||
|
||||
Notes:
|
||||
|
||||
|
@ -108,7 +107,7 @@ Notes:
|
|||
|
||||
| Variable | Description |
|
||||
|-----------------------|:-------------------------|
|
||||
| DISCORD_INVITE | Invite to discord server |
|
||||
| GUILDED_INVITE | Invite to guilded server |
|
||||
| DISCORD_CLIENT_ID | All |
|
||||
| DISCORD_CLIENT_SECRET | those |
|
||||
| GOOGLE_CLIENT_ID | values |
|
||||
|
|
9
deployment/example-ecosystem-captchas.yml
Normal file
9
deployment/example-ecosystem-captchas.yml
Normal 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
|
|
@ -3,9 +3,9 @@ apps:
|
|||
name : 'web'
|
||||
node_args: --nouse-idle-notification --expose-gc
|
||||
env:
|
||||
HOSTURL: "http://localhost"
|
||||
ASSET_SERVER: "http://localhost"
|
||||
PORT: 80
|
||||
HOST: "localhost"
|
||||
ASSET_SERVER: "http://localhost"
|
||||
REDIS_URL: 'redis://localhost:6379'
|
||||
MYSQL_HOST: "localhost"
|
||||
MYSQL_USER: "pixelplanet"
|
||||
|
|
|
@ -42,8 +42,10 @@ do
|
|||
cd "$PFOLDER"
|
||||
pm2 stop web
|
||||
pm2 stop backup
|
||||
pm2 stop captchas
|
||||
pm2 start ecosystem.yml
|
||||
pm2 start ecosystem-backup.yml
|
||||
pm2 start ecosystem-captchas.yml
|
||||
curl -H "Content-Type: application/json" --data-binary '{ "username": "PixelPlanet Server", "avatar_url": "https://pixelplanet.fun/favicon.ico", "content": "...Done", "embeds": [{"title": "New Commits", "url": "https://pixelplanet.fun", "description": "'"$COMMITS"'", "color": 15258703}] }' "$PWEBHOOK"
|
||||
else
|
||||
echo "---UPDATING REPO ON DEV SERVER---"
|
||||
|
|
47
i18n/pt.po
47
i18n/pt.po
|
@ -228,8 +228,8 @@ msgid ""
|
|||
"already set pixels."
|
||||
msgstr ""
|
||||
"Nosso canvas principal é um mapa mundi enorme, que você pode colocar píxeis "
|
||||
"onde quiser, mas você tera que esperar um tempo entre píxeis. Você pode ver "
|
||||
"o tempo de espera entre píxeis e outros requerimentos no menu de Seleção de "
|
||||
"onde quiser, mas você tera que esperar um tempo entre píxeis. Você pode ver o "
|
||||
"tempo de espera entre píxeis e outros requerimentos no menu de Seleção de "
|
||||
"Canvas (o botão com o formato de globo no topo). Alguns canvas tem um tempo "
|
||||
"de espera diferente para trocar píxeis que foram colocados por outro usuário "
|
||||
"do que um píxeis que ainda não foram pintado. Por exemplo: 4s/7s significa 4 "
|
||||
|
@ -265,8 +265,8 @@ msgid ""
|
|||
"The bare map data that we use, together with converted OpenStreetMap tiles "
|
||||
"for orientation, can be downloaded from mega.nz here: "
|
||||
msgstr ""
|
||||
"O mapa original que usamos, junto com os pedaços convertidos do "
|
||||
"OpenStreetMap para orientação, podem ser baixados do mega.nz aqui: "
|
||||
"O mapa original que usamos, junto com os pedaços convertidos do OpenStreetMap "
|
||||
"para orientação, podem ser baixados do mega.nz aqui: "
|
||||
|
||||
#: src/components/HelpModal.jsx:59
|
||||
msgid "Detected as Proxy?"
|
||||
|
@ -365,8 +365,8 @@ msgid ""
|
|||
"Click ${ mouseSymbol } middle mouse button or ${ touchSymbol } long-tap to "
|
||||
"select current hovering color"
|
||||
msgstr ""
|
||||
"Clique ${ mouseSymbol } com botão do meio do mouse ou ${ touchSymbol } "
|
||||
"toque e segure para selecionar a cor que o mouse/seu dedo está em cima"
|
||||
"Clique ${ mouseSymbol } com botão do meio do mouse ou ${ touchSymbol } toque "
|
||||
"e segure para selecionar a cor que o mouse/seu dedo está em cima"
|
||||
|
||||
#: src/components/HelpModal.jsx:84
|
||||
msgid "Press ${ bindE } and ${ bindC } to fly up and down"
|
||||
|
@ -469,8 +469,8 @@ msgid ""
|
|||
"Zoom in instead of placing a pixel when you tap the canvas and your zoom is "
|
||||
"small."
|
||||
msgstr ""
|
||||
"Dar zoom ao invés de colocar um pixel quando você aperta no canvas e seu "
|
||||
"zoom é baixo."
|
||||
"Dar zoom ao invés de colocar um pixel quando você aperta no canvas e seu zoom "
|
||||
"é baixo."
|
||||
|
||||
#: src/components/SettingsModal.jsx:158
|
||||
msgid "Compact Palette"
|
||||
|
@ -478,8 +478,7 @@ msgstr "Paleta Compacta"
|
|||
|
||||
#: src/components/SettingsModal.jsx:160
|
||||
msgid "Display Palette in a compact form that takes less screen space."
|
||||
msgstr ""
|
||||
"Mostrar a paleta em um formato compacto que ocupa menos espaço da tela."
|
||||
msgstr "Mostrar a paleta em um formato compacto que ocupa menos espaço da tela."
|
||||
|
||||
#: src/components/SettingsModal.jsx:165
|
||||
msgid "Potato Mode"
|
||||
|
@ -517,8 +516,8 @@ msgstr "Como pixelplanet deve parecer."
|
|||
msgid "Register new account here"
|
||||
msgstr "Registre uma conta nova aqui"
|
||||
|
||||
#: src/components/ForgotPasswordModal.jsx:20
|
||||
#: src/components/RegisterModal.jsx:21 src/components/UserAreaModal.jsx:130
|
||||
#: src/components/ForgotPasswordModal.jsx:20 src/components/RegisterModal.jsx:21
|
||||
#: src/components/UserAreaModal.jsx:130
|
||||
msgid "Consider joining us on Guilded:"
|
||||
msgstr "Considere entrar no nosso Guilded:"
|
||||
|
||||
|
@ -576,16 +575,16 @@ msgstr "Carregando..."
|
|||
|
||||
#: src/components/ArchiveModal.jsx:20
|
||||
msgid ""
|
||||
"While we tend to not delete canvases, some canvases are started for fun or "
|
||||
"as a request by users who currently like a meme. Those canvases can get "
|
||||
"boring after a while and after weeks of no major change and if they really "
|
||||
"aren't worth being kept active, we decide to remove them."
|
||||
"While we tend to not delete canvases, some canvases are started for fun or as "
|
||||
"a request by users who currently like a meme. Those canvases can get boring "
|
||||
"after a while and after weeks of no major change and if they really aren't "
|
||||
"worth being kept active, we decide to remove them."
|
||||
msgstr ""
|
||||
"Enquanto a gente não costuma deletar os canvas, alguns canvas são feitos "
|
||||
"apenas como piada ou alguns usuários pedem porque gostam de um meme. Esses "
|
||||
"canvas depois de um tempo podem ficar chatos e entediantes e se não forem "
|
||||
"bastante modificados depois de semanas e não valerem a pena se manter "
|
||||
"ativos, nós decidimos removê-los."
|
||||
"bastante modificados depois de semanas e não valerem a pena se manter ativos, "
|
||||
"nós decidimos removê-los."
|
||||
|
||||
#: src/components/ArchiveModal.jsx:22
|
||||
msgid ""
|
||||
|
@ -602,9 +601,9 @@ msgstr "Canvas do Compasso Político"
|
|||
#: src/components/ArchiveModal.jsx:31
|
||||
msgid ""
|
||||
"This canvas got requested during a time of political conflicts on the main "
|
||||
"Earth canvas. It was a 1024x1024 representation of the political compass "
|
||||
"with a 5s cooldown and 60s stacking. It got launched on May 11th and "
|
||||
"remained active for months till it got shut down on November 30th."
|
||||
"Earth canvas. It was a 1024x1024 representation of the political compass with "
|
||||
"a 5s cooldown and 60s stacking. It got launched on May 11th and remained "
|
||||
"active for months till it got shut down on November 30th."
|
||||
msgstr ""
|
||||
"Este canvas foi pedido durante um tempo de muitos conflitos políticos no "
|
||||
"canvas principal, a Terra. Era uma representação com 1024x1024 píxeis do "
|
||||
|
@ -618,8 +617,8 @@ msgid ""
|
|||
"screenshot from the timelapse results in a perfect 1:1 representation of how "
|
||||
"the canvas was at that time."
|
||||
msgstr ""
|
||||
"Nos decidimos arquivá-lo como uma timelapse em um webm codificado sem "
|
||||
"perdas. Tirando uma captura de tela da timelapse resulta em uma perfeita "
|
||||
"Nos decidimos arquivá-lo como uma timelapse em um webm codificado sem perdas. "
|
||||
"Tirando uma captura de tela da timelapse resulta em uma perfeita "
|
||||
"representação 1:1 de como o canvas era naquele tempo."
|
||||
|
||||
#: src/components/ArchiveModal.jsx:50
|
||||
|
@ -666,7 +665,7 @@ msgstr "Nome ou Email"
|
|||
#: src/components/ChangeMail.jsx:112 src/components/DeleteAccount.jsx:89
|
||||
#: src/components/LogInForm.jsx:111 src/components/SignUpForm.jsx:140
|
||||
msgid "Password"
|
||||
msgstr "SeNHA"
|
||||
msgstr "Senha"
|
||||
|
||||
#: src/components/LogInForm.jsx:115
|
||||
msgid "LogIn"
|
||||
|
|
|
@ -73,11 +73,11 @@ msgstr ""
|
|||
msgid "A 3D globe of our whole map"
|
||||
msgstr ""
|
||||
|
||||
#: src/ssr-components/Main.jsx:72
|
||||
#: src/ssr-components/Main.jsx:73
|
||||
msgid "PixelPlanet.fun"
|
||||
msgstr ""
|
||||
|
||||
#: src/ssr-components/Main.jsx:74
|
||||
#: src/ssr-components/Main.jsx:75
|
||||
msgid "Place color pixels on an map styled canvas with other players online"
|
||||
msgstr ""
|
||||
|
||||
|
@ -270,39 +270,28 @@ msgstr ""
|
|||
msgid "Password must be shorter than 60 characters."
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/validation.js:74
|
||||
msgid "Could not connect to server, please try again later :("
|
||||
#: src/routes/api/captcha.js:22
|
||||
msgid "No captcha text given"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/validation.js:80
|
||||
msgid "I think we experienced some error :("
|
||||
#: src/routes/api/captcha.js:36
|
||||
msgid "You took too long, try again."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/change_mail.js:41
|
||||
#: src/routes/api/auth/change_passwd.js:37
|
||||
#: src/routes/api/auth/delete_account.js:38
|
||||
msgid "You are not authenticated."
|
||||
#: src/routes/api/captcha.js:42
|
||||
msgid "You failed your captcha"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/change_mail.js:50
|
||||
#: src/routes/api/auth/change_passwd.js:46
|
||||
#: src/routes/api/auth/delete_account.js:48
|
||||
msgid "Incorrect password!"
|
||||
#: src/routes/api/captcha.js:48
|
||||
msgid "Unknown Captcha Error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/verify.js:25
|
||||
#: src/routes/api/auth/verify.js:32
|
||||
msgid "Mail verification"
|
||||
#: src/routes/api/captcha.js:55
|
||||
msgid "Server error occured"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/verify.js:26
|
||||
msgid "You are now verified :)"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/verify.js:32
|
||||
msgid ""
|
||||
"Your mail verification code is invalid or already expired :(, please "
|
||||
"request a new one."
|
||||
#: src/routes/api/auth/logout.js:13
|
||||
msgid "You are not even logged in."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/register.js:31
|
||||
|
@ -321,8 +310,31 @@ msgstr ""
|
|||
msgid "Failed to establish session after register :("
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/logout.js:13
|
||||
msgid "You are not even logged in."
|
||||
#: src/routes/api/auth/verify.js:25
|
||||
#: src/routes/api/auth/verify.js:32
|
||||
msgid "Mail verification"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/verify.js:26
|
||||
msgid "You are now verified :)"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/verify.js:32
|
||||
msgid ""
|
||||
"Your mail verification code is invalid or already expired :(, please "
|
||||
"request a new one."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/change_mail.js:41
|
||||
#: src/routes/api/auth/change_passwd.js:37
|
||||
#: src/routes/api/auth/delete_account.js:38
|
||||
msgid "You are not authenticated."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/api/auth/change_mail.js:50
|
||||
#: src/routes/api/auth/change_passwd.js:46
|
||||
#: src/routes/api/auth/delete_account.js:48
|
||||
msgid "Incorrect password!"
|
||||
msgstr ""
|
||||
|
||||
#: src/ssr-components/RedirectionPage.jsx:20
|
||||
|
|
File diff suppressed because it is too large
Load Diff
4670
package-lock.json
generated
4670
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
|
@ -10,6 +10,7 @@
|
|||
"main": "server.js",
|
||||
"scripts": {
|
||||
"build": "babel-node scripts/run prebuild && npm run webpack",
|
||||
"build-en": "babel-node scripts/run prebuild && npm run extract",
|
||||
"clean": "babel-node scripts/run clean",
|
||||
"webpack": "webpack --config ./webpack.config.web.babel.js && parallel-webpack --config ./webpack.config.client.babel.js",
|
||||
"extract": "webpack --env extract --config ./webpack.config.web.babel.js && webpack --env extract --config ./webpack.config.client.babel.js",
|
||||
|
@ -27,14 +28,14 @@
|
|||
"not IE_Mob 11"
|
||||
],
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.0.0",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bluebird": "^3.5.0",
|
||||
"body-parser": "^1.17.2",
|
||||
"bufferutil": "^4.0.3",
|
||||
"compression": "^1.7.3",
|
||||
"connect-redis": "^5.0.0",
|
||||
"connect-redis": "^5.1.0",
|
||||
"cookie": "^0.4.1",
|
||||
"core-js": "^3.8.3",
|
||||
"core-js": "^3.9.1",
|
||||
"cors": "^2.8.4",
|
||||
"etag": "^1.8.1",
|
||||
"express": "^4.15.3",
|
||||
|
@ -50,7 +51,7 @@
|
|||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.1",
|
||||
"mysql2": "^2.2.5",
|
||||
"nodemailer": "^6.4.17",
|
||||
"nodemailer": "^6.5.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-discord": "^0.1.4",
|
||||
"passport-facebook": "^3.0.0",
|
||||
|
@ -58,10 +59,10 @@
|
|||
"passport-json": "^1.2.0",
|
||||
"passport-reddit": "^0.2.4",
|
||||
"passport-vkontakte": "^0.5.0",
|
||||
"ppfun-captcha": "^1.6.4",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-icons": "^4.1.0",
|
||||
"react-modal": "^3.12.1",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-responsive": "^8.2.0",
|
||||
"react-stay-scrolled": "^7.3.0",
|
||||
|
@ -72,59 +73,58 @@
|
|||
"redux-logger": "^3.0.6",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"sequelize": "^6.5.0",
|
||||
"sharp": "^0.27.1",
|
||||
"sequelize": "^6.5.1",
|
||||
"sharp": "^0.27.2",
|
||||
"startaudiocontext": "^1.2.1",
|
||||
"sweetalert2": "^10.14.0",
|
||||
"three": "^0.125.2",
|
||||
"three-trackballcontrols": "^0.9.0",
|
||||
"ttag": "^1.7.24",
|
||||
"ttag-po-loader": "0.0.2",
|
||||
"url-search-params-polyfill": "^8.1.0",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.0",
|
||||
"ws": "^7.4.2"
|
||||
"winston-daily-rotate-file": "^4.5.1",
|
||||
"ws": "^7.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/node": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.12.12",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.10.4",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.10.4",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.10.4",
|
||||
"@babel/plugin-proposal-function-bind": "^7.11.5",
|
||||
"@babel/plugin-proposal-function-sent": "^7.10.4",
|
||||
"@babel/plugin-proposal-json-strings": "^7.10.4",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.11.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.12.7",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.10.5",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.10.4",
|
||||
"@babel/core": "^7.13.10",
|
||||
"@babel/node": "^7.13.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.13.5",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.12.13",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.12.13",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.12.13",
|
||||
"@babel/plugin-proposal-function-bind": "^7.12.13",
|
||||
"@babel/plugin-proposal-function-sent": "^7.12.13",
|
||||
"@babel/plugin-proposal-json-strings": "^7.13.8",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.13.8",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.13",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.13.8",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.12.13",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.12.13",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.12.10",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.10.4",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.10.4",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.13.0",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.13.10",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.12.13",
|
||||
"@babel/polyfill": "^7.11.5",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/preset-env": "^7.13.10",
|
||||
"@babel/preset-flow": "^7.12.13",
|
||||
"@babel/preset-react": "^7.12.13",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"assets-webpack-plugin": "^7.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-plugin-transform-react-pure-class-to-function": "^1.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||
"babel-plugin-ttag": "^1.7.30",
|
||||
"clean-css": "^5.0.1",
|
||||
"css-loader": "^5.0.1",
|
||||
"eslint": "^7.19.0",
|
||||
"clean-css": "^5.1.1",
|
||||
"css-loader": "^5.1.3",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-flowtype": "^5.2.0",
|
||||
"eslint-plugin-flowtype": "^5.4.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
|
@ -140,9 +140,9 @@
|
|||
"rimraf": "^3.0.2",
|
||||
"style-loader": "^2.0.0",
|
||||
"ttag-cli": "^1.9.1",
|
||||
"webpack": "^5.19.0",
|
||||
"webpack": "^5.26.3",
|
||||
"webpack-bundle-analyzer": "^4.4.0",
|
||||
"webpack-cli": "^4.4.0",
|
||||
"webpack-cli": "^4.5.0",
|
||||
"webpack-dev-middleware": "^4.1.0",
|
||||
"webpack-hot-middleware": "^2.18.0",
|
||||
"webpack-node-externals": "^2.5.2",
|
||||
|
|
|
@ -92,6 +92,10 @@ async function copy() {
|
|||
`${deploydir}/example-ecosystem-backup.yml`,
|
||||
`${builddir}/ecosystem-backup.example.yml`,
|
||||
),
|
||||
copyFile(
|
||||
`${deploydir}/example-ecosystem-captchas.yml`,
|
||||
`${builddir}/ecosystem-captchas.example.yml`,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
* Collect api fetch commands for actions here
|
||||
* (chunk and tiles requests in ui/ChunkLoader*.js)
|
||||
* (user settings requests in their components)
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import { t } from 'ttag';
|
||||
|
||||
/*
|
||||
* Adds customizeable timeout to fetch
|
||||
|
@ -27,123 +27,226 @@ async function fetchWithTimeout(resource, options) {
|
|||
}
|
||||
|
||||
/*
|
||||
* block / unblock user
|
||||
* userId id of user to block
|
||||
* block true if block, false if unblock
|
||||
* return error string or null if successful
|
||||
* Parse response from API
|
||||
* @param response
|
||||
* @return Object of response
|
||||
*/
|
||||
export async function requestBlock(userId: number, block: boolean) {
|
||||
const response = await fetchWithTimeout('api/block', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId,
|
||||
block,
|
||||
}),
|
||||
});
|
||||
async function parseAPIresponse(response) {
|
||||
const { status: code } = response;
|
||||
|
||||
if (code === 429) {
|
||||
let error = t`You made too many requests`;
|
||||
const retryAfter = response.headers.get('Retry-After');
|
||||
if (!Number.isNaN(Number(retryAfter))) {
|
||||
const ti = Math.floor(retryAfter / 60);
|
||||
error += `, ${t`try again after ${ti}min`}`;
|
||||
}
|
||||
return {
|
||||
errors: [error],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await response.json();
|
||||
if (res.errors) {
|
||||
return res.errors[0];
|
||||
}
|
||||
if (response.ok && res.status === 'ok') {
|
||||
return null;
|
||||
}
|
||||
return 'Unknown Error';
|
||||
} catch {
|
||||
return 'Connection Error';
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
return {
|
||||
errors: [t`Connection error ${code} :(`],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Make API POST Request
|
||||
* @param url URL of post api endpoint
|
||||
* @param body Body of request
|
||||
* @return Object with response or error Array
|
||||
*/
|
||||
async function makeAPIPOSTRequest(url, body) {
|
||||
try {
|
||||
const response = await fetchWithTimeout(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return parseAPIresponse(response);
|
||||
} catch (e) {
|
||||
return {
|
||||
errors: [t`Could not connect to server, please try again later :(`],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Make API GET Request
|
||||
* @param url URL of get api endpoint
|
||||
* @return Object with response or error Array
|
||||
*/
|
||||
async function makeAPIGETRequest(url) {
|
||||
try {
|
||||
const response = await fetchWithTimeout(url, {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
return parseAPIresponse(response);
|
||||
} catch (e) {
|
||||
return {
|
||||
errors: [t`Could not connect to server, please try again later :(`],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* block / unblock user
|
||||
* @param userId id of user to block
|
||||
* @param block true if block, false if unblock
|
||||
* @return error string or null if successful
|
||||
*/
|
||||
export async function requestBlock(userId: number, block: boolean) {
|
||||
const res = await makeAPIPOSTRequest(
|
||||
'api/block',
|
||||
{ userId, block },
|
||||
);
|
||||
if (res.errors) {
|
||||
return res.errors[0];
|
||||
}
|
||||
if (res.status === 'ok') {
|
||||
return null;
|
||||
}
|
||||
return t`Unknown Error`;
|
||||
}
|
||||
|
||||
/*
|
||||
* start new DM channel with user
|
||||
* query Object with either userId: number or userName: string
|
||||
* return channel Array on success, error string if not
|
||||
* @param query Object with either userId: number or userName: string
|
||||
* @return channel Array on success, error string if not
|
||||
*/
|
||||
export async function requestStartDm(query) {
|
||||
const response = await fetchWithTimeout('api/startdm', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await response.json();
|
||||
if (res.errors) {
|
||||
return res.errors[0];
|
||||
}
|
||||
if (response.ok && res.channel) {
|
||||
const { channel } = res;
|
||||
return channel;
|
||||
}
|
||||
|
||||
return 'Unknown Error';
|
||||
} catch {
|
||||
return 'Connection Error';
|
||||
const res = await makeAPIPOSTRequest(
|
||||
'api/startdm',
|
||||
query,
|
||||
);
|
||||
if (res.errors) {
|
||||
return res.errors[0];
|
||||
}
|
||||
if (res.channel) {
|
||||
return res.channel;
|
||||
}
|
||||
return t`Unknown Error`;
|
||||
}
|
||||
|
||||
/*
|
||||
* set receiving of all DMs on/off
|
||||
* block true if blocking all dms, false if unblocking
|
||||
* return error string or null if successful
|
||||
* @param block true if blocking all dms, false if unblocking
|
||||
* @return error string or null if successful
|
||||
*/
|
||||
export async function requestBlockDm(block: boolean) {
|
||||
const response = await fetchWithTimeout('api/blockdm', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ block }),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await response.json();
|
||||
if (res.errors) {
|
||||
return res.errors[0];
|
||||
}
|
||||
if (response.ok && res.status === 'ok') {
|
||||
return null;
|
||||
}
|
||||
return 'Unknown Error';
|
||||
} catch {
|
||||
return 'Connection Error';
|
||||
const res = await makeAPIPOSTRequest(
|
||||
'api/blockdm',
|
||||
{ block },
|
||||
);
|
||||
if (res.errors) {
|
||||
return res.errors[0];
|
||||
}
|
||||
if (res.status === 'ok') {
|
||||
return null;
|
||||
}
|
||||
return t`Unknown Error`;
|
||||
}
|
||||
|
||||
/*
|
||||
* leaving Chat Channel (i.e. DM channel)
|
||||
* channelId 8nteger id of channel
|
||||
* return error string or null if successful
|
||||
* @param channelId 8nteger id of channel
|
||||
* @return error string or null if successful
|
||||
*/
|
||||
export async function requestLeaveChan(channelId: boolean) {
|
||||
const response = await fetchWithTimeout('api/leavechan', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ channelId }),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await response.json();
|
||||
if (res.errors) {
|
||||
return res.errors[0];
|
||||
}
|
||||
if (response.ok && res.status === 'ok') {
|
||||
return null;
|
||||
}
|
||||
return 'Unknown Error';
|
||||
} catch {
|
||||
return 'Connection Error';
|
||||
const res = await makeAPIPOSTRequest(
|
||||
'api/leavechan',
|
||||
{ channelId },
|
||||
);
|
||||
if (res.errors) {
|
||||
return res.errors[0];
|
||||
}
|
||||
if (res.status === 'ok') {
|
||||
return null;
|
||||
}
|
||||
return t`Unknown Error`;
|
||||
}
|
||||
|
||||
export async function requestSolveCaptcha(text) {
|
||||
const res = await makeAPIPOSTRequest(
|
||||
'api/captcha',
|
||||
{ text },
|
||||
);
|
||||
if (!res.errors && !res.success) {
|
||||
return {
|
||||
errors: [t`Server answered with gibberish :(`],
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function requestPasswordChange(newPassword, password) {
|
||||
return makeAPIPOSTRequest(
|
||||
'api/auth/change_passwd',
|
||||
{ password, newPassword },
|
||||
);
|
||||
}
|
||||
|
||||
export async function requestResendVerify() {
|
||||
return makeAPIGETRequest(
|
||||
'./api/auth/resend_verify',
|
||||
);
|
||||
}
|
||||
|
||||
export function requestMcLink(accepted) {
|
||||
return makeAPIPOSTRequest(
|
||||
'api/auth/mclink',
|
||||
{ accepted },
|
||||
);
|
||||
}
|
||||
|
||||
export function requestNameChange(name) {
|
||||
return makeAPIPOSTRequest(
|
||||
'api/auth/change_name',
|
||||
{ name },
|
||||
);
|
||||
}
|
||||
|
||||
export function requestMailChange(email, password) {
|
||||
return makeAPIPOSTRequest(
|
||||
'api/auth/change_mail',
|
||||
{ email, password },
|
||||
);
|
||||
}
|
||||
|
||||
export function requestLogin(nameoremail, password) {
|
||||
return makeAPIPOSTRequest(
|
||||
'api/auth/local',
|
||||
{ nameoremail, password },
|
||||
);
|
||||
}
|
||||
|
||||
export function requestRegistration(name, email, password) {
|
||||
return makeAPIPOSTRequest(
|
||||
'api/auth/register',
|
||||
{ name, email, password },
|
||||
);
|
||||
}
|
||||
|
||||
export function requestNewPassword(email) {
|
||||
return makeAPIPOSTRequest(
|
||||
'api/auth/restore_password',
|
||||
{ email },
|
||||
);
|
||||
}
|
||||
|
||||
export function requestDeleteAccount(password) {
|
||||
return makeAPIPOSTRequest(
|
||||
'api/auth/delete_account',
|
||||
{ password },
|
||||
);
|
||||
}
|
||||
|
|
|
@ -29,6 +29,12 @@ export function sweetAlert(
|
|||
};
|
||||
}
|
||||
|
||||
export function closeAlert(): Action {
|
||||
return {
|
||||
type: 'CLOSE_ALERT',
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleChatBox(): Action {
|
||||
return {
|
||||
type: 'TOGGLE_CHAT_BOX',
|
||||
|
@ -114,9 +120,9 @@ export function toggleOpenMenu(): Action {
|
|||
};
|
||||
}
|
||||
|
||||
export function setPlaceAllowed(requestingPixel: boolean): Action {
|
||||
export function setRequestingPixel(requestingPixel: boolean): Action {
|
||||
return {
|
||||
type: 'SET_PLACE_ALLOWED',
|
||||
type: 'SET_REQUESTING_PIXEL',
|
||||
requestingPixel,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export type Action =
|
|||
icon: string,
|
||||
confirmButtonText: string,
|
||||
}
|
||||
| { type: 'CLOSE_ALERT' }
|
||||
| { type: 'TOGGLE_GRID' }
|
||||
| { type: 'TOGGLE_PIXEL_NOTIFY' }
|
||||
| { type: 'TOGGLE_AUTO_ZOOM_IN' }
|
||||
|
@ -28,7 +29,7 @@ export type Action =
|
|||
| { type: 'SELECT_STYLE', style: string }
|
||||
| { type: 'SET_NOTIFICATION', notification: string }
|
||||
| { type: 'UNSET_NOTIFICATION' }
|
||||
| { type: 'SET_PLACE_ALLOWED', requestingPixel: boolean }
|
||||
| { type: 'SET_REQUESTING_PIXEL', requestingPixel: boolean }
|
||||
| { type: 'SET_HOVER', hover: Cell }
|
||||
| { type: 'UNSET_HOVER' }
|
||||
| { type: 'SET_WAIT', wait: ?number }
|
||||
|
|
45
src/captchaserver.js
Normal file
45
src/captchaserver.js
Normal 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
74
src/components/Alert.jsx
Normal 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
127
src/components/Captcha.jsx
Normal 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>: {error}
|
||||
</p>
|
||||
))}
|
||||
<p className="modaltext">
|
||||
{t`Type the characters from the following image:`}
|
||||
|
||||
<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:`}
|
||||
<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>
|
||||
|
||||
<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);
|
|
@ -6,8 +6,9 @@
|
|||
import React from 'react';
|
||||
import { t } from 'ttag';
|
||||
import {
|
||||
validateEMail, validatePassword, parseAPIresponse,
|
||||
validateEMail, validatePassword,
|
||||
} from '../utils/validation';
|
||||
import { requestMailChange } from '../actions/fetch';
|
||||
|
||||
function validate(email, password) {
|
||||
const errors = [];
|
||||
|
@ -20,23 +21,6 @@ function validate(email, password) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
async function submitMailchange(email, password) {
|
||||
const body = JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
const response = await fetch('./api/auth/change_mail', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
return parseAPIresponse(response);
|
||||
}
|
||||
|
||||
class ChangeMail extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -64,7 +48,7 @@ class ChangeMail extends React.Component {
|
|||
if (errors.length > 0) return;
|
||||
this.setState({ submitting: true });
|
||||
|
||||
const { errors: resperrors } = await submitMailchange(email, password);
|
||||
const { errors: resperrors } = await requestMailChange(email, password);
|
||||
if (resperrors) {
|
||||
this.setState({
|
||||
errors: resperrors,
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
import React from 'react';
|
||||
import { t } from 'ttag';
|
||||
import { validateName, parseAPIresponse } from '../utils/validation';
|
||||
import { validateName } from '../utils/validation';
|
||||
import { requestNameChange } from '../actions/fetch';
|
||||
|
||||
|
||||
function validate(name) {
|
||||
|
@ -17,22 +18,6 @@ function validate(name) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
async function submitNamechange(name) {
|
||||
const body = JSON.stringify({
|
||||
name,
|
||||
});
|
||||
const response = await fetch('./api/auth/change_name', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
return parseAPIresponse(response);
|
||||
}
|
||||
|
||||
class ChangeName extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -58,7 +43,7 @@ class ChangeName extends React.Component {
|
|||
if (errors.length > 0) return;
|
||||
this.setState({ submitting: true });
|
||||
|
||||
const { errors: resperrors } = await submitNamechange(name);
|
||||
const { errors: resperrors } = await requestNameChange(name);
|
||||
if (resperrors) {
|
||||
this.setState({
|
||||
errors: resperrors,
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { t } from 'ttag';
|
||||
import { validatePassword, parseAPIresponse } from '../utils/validation';
|
||||
import { validatePassword } from '../utils/validation';
|
||||
import { requestPasswordChange } from '../actions/fetch';
|
||||
|
||||
function validate(mailreg, password, newPassword, confirmPassword) {
|
||||
const errors = [];
|
||||
|
@ -24,136 +25,92 @@ function validate(mailreg, password, newPassword, confirmPassword) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
async function submitPasswordChange(newPassword, password) {
|
||||
const body = JSON.stringify({
|
||||
password,
|
||||
newPassword,
|
||||
});
|
||||
const response = await fetch('./api/auth/change_passwd', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body,
|
||||
credentials: 'include',
|
||||
});
|
||||
const ChangePassword = ({ mailreg, done, cancel }) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [errors, setErrors] = useState([]);
|
||||
|
||||
return parseAPIresponse(response);
|
||||
}
|
||||
|
||||
|
||||
class ChangePassword extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
password: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
success: false,
|
||||
submitting: false,
|
||||
|
||||
errors: [],
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
async handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const {
|
||||
password, newPassword, confirmPassword, submitting,
|
||||
} = this.state;
|
||||
if (submitting) return;
|
||||
|
||||
const { mailreg } = this.props;
|
||||
const errors = validate(
|
||||
mailreg,
|
||||
password,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
);
|
||||
|
||||
this.setState({ errors });
|
||||
if (errors.length > 0) return;
|
||||
this.setState({ submitting: true });
|
||||
|
||||
const { errors: resperrors } = await submitPasswordChange(
|
||||
newPassword,
|
||||
password,
|
||||
);
|
||||
if (resperrors) {
|
||||
this.setState({
|
||||
errors: resperrors,
|
||||
submitting: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { success } = this.state;
|
||||
if (success) {
|
||||
const { done } = this.props;
|
||||
return (
|
||||
<div className="inarea">
|
||||
<p className="modalmessage">{t`Changed Password successfully.`}</p>
|
||||
<button type="button" onClick={done}>Close</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const {
|
||||
errors,
|
||||
password,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
submitting,
|
||||
} = this.state;
|
||||
const { cancel, mailreg } = this.props;
|
||||
if (success) {
|
||||
return (
|
||||
<div className="inarea">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errors.map((error) => (
|
||||
<p key={error} className="errormessage"><span>{t`Error`}</span>
|
||||
: {error}</p>
|
||||
))}
|
||||
{(mailreg)
|
||||
&& (
|
||||
<input
|
||||
value={password}
|
||||
onChange={(evt) => this.setState({ password: evt.target.value })}
|
||||
type="password"
|
||||
placeholder={t`Old Password`}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
<input
|
||||
value={newPassword}
|
||||
onChange={(evt) => this.setState({ newPassword: evt.target.value })}
|
||||
type="password"
|
||||
placeholder={t`New Password`}
|
||||
/>
|
||||
<br />
|
||||
<input
|
||||
value={confirmPassword}
|
||||
onChange={(evt) => this.setState({
|
||||
confirmPassword: evt.target.value,
|
||||
})}
|
||||
type="password"
|
||||
placeholder={t`Confirm New Password`}
|
||||
/>
|
||||
<br />
|
||||
<button type="submit">
|
||||
{(submitting) ? '...' : t`Save`}
|
||||
</button>
|
||||
<button type="button" onClick={cancel}>{t`Cancel`}</button>
|
||||
</form>
|
||||
<p className="modalmessage">{t`Changed Password successfully.`}</p>
|
||||
<button type="button" onClick={done}>Close</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChangePassword;
|
||||
return (
|
||||
<div className="inarea">
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
if (submitting) return;
|
||||
const valerrors = validate(
|
||||
mailreg,
|
||||
password,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
);
|
||||
setErrors(valerrors);
|
||||
if (valerrors.length) return;
|
||||
setSubmitting(true);
|
||||
const { errors: resperrors } = await requestPasswordChange(
|
||||
newPassword,
|
||||
password,
|
||||
);
|
||||
if (resperrors) {
|
||||
setErrors(resperrors);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
setSuccess(true);
|
||||
}}
|
||||
>
|
||||
{errors.map((error) => (
|
||||
<p key={error} className="errormessage"><span>{t`Error`}</span>
|
||||
: {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);
|
||||
|
|
|
@ -4,21 +4,21 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import type { State } from '../reducers';
|
||||
import useWindowSize from '../utils/reactHookResize';
|
||||
import { showChatModal } from '../actions';
|
||||
|
||||
import Chat from './Chat';
|
||||
|
||||
|
||||
function ChatBox({
|
||||
chatOpen,
|
||||
triggerModal,
|
||||
}) {
|
||||
const ChatBox = () => {
|
||||
const [render, setRender] = useState(false);
|
||||
|
||||
const chatOpen = useSelector((state) => state.modal.chatOpen);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
window.setTimeout(() => {
|
||||
if (chatOpen) setRender(true);
|
||||
|
@ -31,7 +31,7 @@ function ChatBox({
|
|||
|
||||
const { width } = useWindowSize();
|
||||
if (width < 604 && chatOpen) {
|
||||
triggerModal();
|
||||
dispatch(showChatModal(true));
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -44,19 +44,6 @@ function ChatBox({
|
|||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const { chatOpen } = state.modal;
|
||||
return { chatOpen };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
triggerModal() {
|
||||
dispatch(showChatModal(true));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatBox);
|
||||
export default React.memo(ChatBox);
|
||||
|
|
|
@ -34,7 +34,7 @@ const DailyRankings = ({ totalDailyRanking }) => (
|
|||
);
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const { totalDailyRanking } = state.user;
|
||||
const { totalDailyRanking } = state.ranks;
|
||||
return { totalDailyRanking };
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { t } from 'ttag';
|
||||
|
||||
import { validatePassword, parseAPIresponse } from '../utils/validation';
|
||||
import { validatePassword } from '../utils/validation';
|
||||
import { requestDeleteAccount } from '../actions/fetch';
|
||||
import { logoutUser } from '../actions';
|
||||
|
||||
function validate(password) {
|
||||
|
@ -19,22 +20,6 @@ function validate(password) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
async function submitDeleteAccount(password) {
|
||||
const body = JSON.stringify({
|
||||
password,
|
||||
});
|
||||
const response = await fetch('./api/auth/delete_account', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
return parseAPIresponse(response);
|
||||
}
|
||||
|
||||
class DeleteAccount extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -60,7 +45,7 @@ class DeleteAccount extends React.Component {
|
|||
if (errors.length > 0) return;
|
||||
this.setState({ submitting: true });
|
||||
|
||||
const { errors: resperrors } = await submitDeleteAccount(password);
|
||||
const { errors: resperrors } = await requestDeleteAccount(password);
|
||||
if (resperrors) {
|
||||
this.setState({
|
||||
errors: resperrors,
|
||||
|
|
|
@ -31,11 +31,6 @@ const HelpModal = () => {
|
|||
const bindShift = <kbd>⇧ {c('keybinds').t`Shift`}</kbd>;
|
||||
const bindC = <kbd>{c('keybinds').t`C`}</kbd>;
|
||||
|
||||
const hCaptchaPP = <a href="https://hcaptcha.com/privacy">{t`Privacy Policy`}</a>;
|
||||
const reCaptchaPP = <a href="https://policies.google.com/privacy">{t`Privacy Policy`}</a>;
|
||||
const hCaptchaTOS = <a href="https://hcaptcha.com/terms">{t`Terms of Service`}</a>;
|
||||
const reCaptchaTOS = <a href="https://policies.google.com/terms">{t`Terms of Service`}</a>;
|
||||
|
||||
const guildedLink = <a href="https://pixelplanet.fun/guilded">guilded</a>;
|
||||
const getIPLink = <a href="https://www.whatismyip.com/">{t`your IP`}</a>;
|
||||
const mailLink = <a href="mailto:pixelplanetdev@gmail.com">pixelplanetdev@gmail.com</a>;
|
||||
|
@ -90,20 +85,6 @@ can be downloaded from mega.nz here: `}<a href="https://mega.nz/#!JpkBwAbJ!EnSLl
|
|||
{jt`Click ${mouseSymbol} middle mouse button or ${touchSymbol} long-tap to select current hovering color`}<br />
|
||||
</div>
|
||||
<p>{t`Partners:`} <a href="https://www.crazygames.com/c/io" target="_blank" rel="noopener noreferrer">crazygames.com</a></p>
|
||||
{ (typeof window.hcaptcha === 'undefined')
|
||||
? (
|
||||
<p className="modaltext">
|
||||
<small>
|
||||
{jt`This site is protected by reCAPTCHA and the Google ${reCaptchaPP} and ${reCaptchaTOS} apply.`}
|
||||
</small>
|
||||
</p>
|
||||
) : (
|
||||
<p className="modaltext">
|
||||
<small>
|
||||
{jt`This site is protected by hCAPTCHA and its ${hCaptchaPP} and ${hCaptchaTOS} apply.`}
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,8 +7,9 @@ import { connect } from 'react-redux';
|
|||
import { t } from 'ttag';
|
||||
|
||||
import {
|
||||
validateEMail, validateName, validatePassword, parseAPIresponse,
|
||||
validateEMail, validateName, validatePassword,
|
||||
} from '../utils/validation';
|
||||
import { requestLogin } from '../actions/fetch';
|
||||
import { loginUser } from '../actions';
|
||||
|
||||
|
||||
|
@ -24,22 +25,6 @@ function validate(nameoremail, password) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
async function submitLogin(nameoremail, password) {
|
||||
const body = JSON.stringify({
|
||||
nameoremail,
|
||||
password,
|
||||
});
|
||||
const response = await fetch('./api/auth/local', {
|
||||
method: 'POST',
|
||||
body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return parseAPIresponse(response);
|
||||
}
|
||||
|
||||
const inputStyles = {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
|
@ -73,7 +58,7 @@ class LogInForm extends React.Component {
|
|||
if (errors.length > 0) return;
|
||||
|
||||
this.setState({ submitting: true });
|
||||
const { errors: resperrors, me } = await submitLogin(
|
||||
const { errors: resperrors, me } = await requestLogin(
|
||||
nameoremail,
|
||||
password,
|
||||
);
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { MdClose } from 'react-icons/md';
|
||||
import { t } from 'ttag';
|
||||
|
||||
|
@ -38,44 +37,62 @@ const MODAL_COMPONENTS = {
|
|||
/* other modals */
|
||||
};
|
||||
|
||||
const ModalRoot = ({ modalType, modalOpen, close }) => {
|
||||
const choice = MODAL_COMPONENTS[modalType || 'NONE'];
|
||||
const { content: SpecificModal, title } = choice;
|
||||
const ModalRoot = () => {
|
||||
const [render, setRender] = useState(false);
|
||||
|
||||
const {
|
||||
modalType,
|
||||
modalOpen,
|
||||
} = useSelector((state) => state.modal);
|
||||
|
||||
const {
|
||||
title,
|
||||
content: SpecificModal,
|
||||
} = MODAL_COMPONENTS[modalType || 'NONE'];
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const close = useCallback(() => {
|
||||
dispatch(hideModal());
|
||||
}, [dispatch]);
|
||||
|
||||
const onTransitionEnd = () => {
|
||||
if (!modalOpen) setRender(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.setTimeout(() => {
|
||||
if (modalOpen) setRender(true);
|
||||
}, 10);
|
||||
}, [modalOpen]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalOpen}
|
||||
onClose={close}
|
||||
className="Modal"
|
||||
overlayClassName="Overlay"
|
||||
contentLabel={`${title} Modal`}
|
||||
closeTimeoutMS={200}
|
||||
onRequestClose={close}
|
||||
>
|
||||
<h2 style={{ paddingLeft: '5%' }}>{title}</h2>
|
||||
<div
|
||||
onClick={close}
|
||||
className="ModalClose"
|
||||
role="button"
|
||||
label="close"
|
||||
title={t`Close`}
|
||||
tabIndex={-1}
|
||||
><MdClose /></div>
|
||||
<SpecificModal />
|
||||
</Modal>
|
||||
(render || modalOpen) && (
|
||||
<div>
|
||||
<div
|
||||
className={(modalOpen && render)
|
||||
? 'OverlayModal show'
|
||||
: 'OverlayModal'}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
tabIndex={-1}
|
||||
onClick={close}
|
||||
/>
|
||||
<div
|
||||
className={(modalOpen && render) ? 'Modal show' : 'Modal'}
|
||||
>
|
||||
<h2 style={{ paddingLeft: '5%' }}>{title}</h2>
|
||||
<div
|
||||
onClick={close}
|
||||
className="ModalClose"
|
||||
role="button"
|
||||
label="close"
|
||||
title={t`Close`}
|
||||
tabIndex={-1}
|
||||
><MdClose /></div>
|
||||
<SpecificModal />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const { modalType, modalOpen } = state.modal;
|
||||
return { modalType, modalOpen };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
close() {
|
||||
dispatch(hideModal());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot);
|
||||
export default React.memo(ModalRoot);
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { t } from 'ttag';
|
||||
import { validateEMail, parseAPIresponse } from '../utils/validation';
|
||||
import { validateEMail } from '../utils/validation';
|
||||
import { requestNewPassword } from '../actions/fetch';
|
||||
|
||||
function validate(email) {
|
||||
const errors = [];
|
||||
|
@ -13,21 +14,6 @@ function validate(email) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
async function submitNewpass(email) {
|
||||
const body = JSON.stringify({
|
||||
email,
|
||||
});
|
||||
const response = await fetch('./api/auth/restore_password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
return parseAPIresponse(response);
|
||||
}
|
||||
|
||||
const inputStyles = {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
|
@ -60,7 +46,7 @@ class NewPasswordForm extends React.Component {
|
|||
if (errors.length > 0) return;
|
||||
|
||||
this.setState({ submitting: true });
|
||||
const { errors: resperrors } = await submitNewpass(email);
|
||||
const { errors: resperrors } = await requestNewPassword(email);
|
||||
if (resperrors) {
|
||||
this.setState({
|
||||
errors: resperrors,
|
||||
|
|
|
@ -7,8 +7,9 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { t } from 'ttag';
|
||||
import {
|
||||
validateEMail, validateName, validatePassword, parseAPIresponse,
|
||||
validateEMail, validateName, validatePassword,
|
||||
} from '../utils/validation';
|
||||
import { requestRegistration } from '../actions/fetch';
|
||||
|
||||
import { showUserAreaModal, loginUser } from '../actions';
|
||||
|
||||
|
@ -28,25 +29,6 @@ function validate(name, email, password, confirmPassword) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
|
||||
async function submitRegistration(name, email, password) {
|
||||
const body = JSON.stringify({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
const response = await fetch('./api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
return parseAPIresponse(response);
|
||||
}
|
||||
|
||||
const inputStyles = {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
|
@ -83,7 +65,7 @@ class SignUpForm extends React.Component {
|
|||
if (errors.length > 0) return;
|
||||
|
||||
this.setState({ submitting: true });
|
||||
const { errors: resperrors, me } = await submitRegistration(
|
||||
const { errors: resperrors, me } = await requestRegistration(
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
|
@ -121,6 +103,7 @@ class SignUpForm extends React.Component {
|
|||
<input
|
||||
style={inputStyles}
|
||||
value={name}
|
||||
autoComplete="username"
|
||||
onChange={(evt) => this.setState({ name: evt.target.value })}
|
||||
type="text"
|
||||
placeholder={t`Name`}
|
||||
|
@ -128,6 +111,7 @@ class SignUpForm extends React.Component {
|
|||
<input
|
||||
style={inputStyles}
|
||||
value={email}
|
||||
autoComplete="email"
|
||||
onChange={(evt) => this.setState({ email: evt.target.value })}
|
||||
type="text"
|
||||
placeholder={t`Email`}
|
||||
|
@ -135,6 +119,7 @@ class SignUpForm extends React.Component {
|
|||
<input
|
||||
style={inputStyles}
|
||||
value={password}
|
||||
autoComplete="new-password"
|
||||
onChange={(evt) => this.setState({ password: evt.target.value })}
|
||||
type="password"
|
||||
placeholder={t`Password`}
|
||||
|
@ -142,6 +127,7 @@ class SignUpForm extends React.Component {
|
|||
<input
|
||||
style={inputStyles}
|
||||
value={confirmPassword}
|
||||
autoComplete="new-password"
|
||||
onChange={(evt) => this.setState({
|
||||
confirmPassword: evt.target.value,
|
||||
})}
|
||||
|
|
|
@ -1,36 +1,20 @@
|
|||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
class Tab extends Component {
|
||||
onClick = () => {
|
||||
const { label, onClick } = this.props;
|
||||
onClick(label);
|
||||
const Tab = ({ onClick, activeTab, label }) => {
|
||||
let className = 'tab-list-item';
|
||||
if (activeTab === label) {
|
||||
className += ' tab-list-active';
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onClick,
|
||||
props: {
|
||||
activeTab,
|
||||
label,
|
||||
},
|
||||
} = this;
|
||||
|
||||
let className = 'tab-list-item';
|
||||
|
||||
if (activeTab === label) {
|
||||
className += ' tab-list-active';
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
role="presentation"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li
|
||||
role="presentation"
|
||||
className={className}
|
||||
onClick={() => onClick(label)}
|
||||
>
|
||||
{label}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab;
|
||||
|
|
|
@ -1,62 +1,39 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import Tab from './Tab';
|
||||
|
||||
class Tabs extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const Tabs = ({ children }) => {
|
||||
const [activeTab, setActiveTab] = useState(children[0].props.label);
|
||||
|
||||
const { children } = this.props;
|
||||
this.state = {
|
||||
activeTab: children[0].props.label,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div className="tabs">
|
||||
<ol className="tab-list">
|
||||
{children.map((child) => {
|
||||
if (!child.props) {
|
||||
return undefined;
|
||||
}
|
||||
const { label } = child.props;
|
||||
|
||||
onClickTabItem = (tab) => {
|
||||
this.setState({ activeTab: tab });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onClickTabItem,
|
||||
props: {
|
||||
children,
|
||||
},
|
||||
state: {
|
||||
activeTab,
|
||||
},
|
||||
} = this;
|
||||
|
||||
return (
|
||||
<div className="tabs">
|
||||
<ol className="tab-list">
|
||||
{children.map((child) => {
|
||||
if (!child.props) {
|
||||
return undefined;
|
||||
}
|
||||
const { label } = child.props;
|
||||
|
||||
return (
|
||||
<Tab
|
||||
activeTab={activeTab}
|
||||
key={label}
|
||||
label={label}
|
||||
onClick={onClickTabItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<div className="tab-content">
|
||||
{children.map((child) => {
|
||||
if (!child.props || child.props.label !== activeTab) {
|
||||
return undefined;
|
||||
}
|
||||
return child.props.children;
|
||||
})}
|
||||
</div>
|
||||
return (
|
||||
<Tab
|
||||
activeTab={activeTab}
|
||||
key={label}
|
||||
label={label}
|
||||
onClick={(tab) => setActiveTab(tab)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<div className="tab-content">
|
||||
{children.map((child) => {
|
||||
if (!child.props || child.props.label !== activeTab) {
|
||||
return undefined;
|
||||
}
|
||||
return child.props.children;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
|
|
|
@ -34,7 +34,7 @@ const TotalRankings = ({ totalRanking }) => (
|
|||
);
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const { totalRanking } = state.user;
|
||||
const { totalRanking } = state.ranks;
|
||||
return { totalRanking };
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import NotifyBox from './NotifyBox';
|
|||
import GlobeButton from './GlobeButton';
|
||||
import PalselButton from './PalselButton';
|
||||
import Palette from './Palette';
|
||||
import Alert from './Alert';
|
||||
import HistorySelect from './HistorySelect';
|
||||
import Mobile3DControls from './Mobile3DControls';
|
||||
import UserContextMenu from './UserContextMenu';
|
||||
|
@ -41,13 +42,14 @@ const UI = ({
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<Alert />
|
||||
<PalselButton />
|
||||
<Palette />
|
||||
{(is3D) ? null : <GlobeButton />}
|
||||
{(is3D && isOnMobile) ? <Mobile3DControls /> : null}
|
||||
{(!is3D) && <GlobeButton />}
|
||||
{(is3D && isOnMobile) && <Mobile3DControls />}
|
||||
<CoolDownBox />
|
||||
<NotifyBox />
|
||||
{(menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null}
|
||||
{(menuOpen && menuType) && CONTEXT_MENUS[menuType]}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -183,25 +183,6 @@ class UserArea extends React.Component {
|
|||
done={() => { this.setState({ socialSettingsExtended: false }); }}
|
||||
/>
|
||||
)}
|
||||
{(typeof window.hcaptcha !== 'undefined')
|
||||
&& (
|
||||
<img
|
||||
role="presentation"
|
||||
src="hcaptcha.svg"
|
||||
alt="hCaptcha"
|
||||
title="test hCaptcha"
|
||||
onClick={() => {
|
||||
window.pixel = null;
|
||||
window.hcaptcha.execute();
|
||||
}}
|
||||
style={{
|
||||
width: '5%',
|
||||
height: '5%',
|
||||
paddingTop: 20,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -212,11 +193,13 @@ function mapStateToProps(state: State) {
|
|||
const {
|
||||
name,
|
||||
mailreg,
|
||||
} = state.user;
|
||||
const {
|
||||
totalPixels,
|
||||
dailyTotalPixels,
|
||||
ranking,
|
||||
dailyRanking,
|
||||
} = state.user;
|
||||
} = state.ranks;
|
||||
const stats = {
|
||||
totalPixels,
|
||||
dailyTotalPixels,
|
||||
|
|
|
@ -6,8 +6,8 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { t } from 'ttag';
|
||||
|
||||
import { parseAPIresponse } from '../utils/validation';
|
||||
import { setMinecraftName, remFromMessages } from '../actions';
|
||||
import { requestResendVerify, requestMcLink } from '../actions/fetch';
|
||||
|
||||
|
||||
class UserMessages extends React.Component {
|
||||
|
@ -31,11 +31,8 @@ class UserMessages extends React.Component {
|
|||
resentVerify: true,
|
||||
});
|
||||
|
||||
const response = await fetch('./api/auth/resend_verify', {
|
||||
credentials: 'include',
|
||||
});
|
||||
const { errors } = await requestResendVerify();
|
||||
|
||||
const { errors } = await parseAPIresponse(response);
|
||||
const verifyAnswer = (errors)
|
||||
? errors[0]
|
||||
: t`A new verification mail is getting sent to you.`;
|
||||
|
@ -50,15 +47,9 @@ class UserMessages extends React.Component {
|
|||
this.setState({
|
||||
sentLink: true,
|
||||
});
|
||||
const body = JSON.stringify({ accepted });
|
||||
const rep = await fetch('./api/auth/mclink', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const { errors } = parseAPIresponse(rep);
|
||||
const { errors } = await requestMcLink(accepted);
|
||||
|
||||
if (errors) {
|
||||
this.setState({
|
||||
linkAnswer: errors[0],
|
||||
|
|
|
@ -10,6 +10,7 @@ if (process.env.BROWSER) {
|
|||
}
|
||||
|
||||
export const PORT = process.env.PORT || 80;
|
||||
export const HOST = process.env.HOST || 'localhost';
|
||||
|
||||
export const GMAIL_USER = process.env.GMAIL_USER || null;
|
||||
export const GMAIL_PW = process.env.GMAIL_PW || null;
|
||||
|
@ -19,6 +20,8 @@ export const TILE_FOLDER = path.join(__dirname, `./${TILE_FOLDER_REL}`);
|
|||
|
||||
export const ASSET_SERVER = process.env.ASSET_SERVER || '.';
|
||||
|
||||
export const CAPTCHA_URL = process.env.CAPTCHA_URL || '';
|
||||
|
||||
export const USE_XREALIP = process.env.USE_XREALIP || false;
|
||||
|
||||
export const BACKUP_URL = process.env.BACKUP_URL || null;
|
||||
|
@ -35,8 +38,6 @@ export const MYSQL_USER = process.env.MYSQL_USER || 'pixelplanet';
|
|||
export const MYSQL_PW = process.env.MYSQL_PW || 'password';
|
||||
|
||||
// Social
|
||||
export const DISCORD_INVITE = process.env.DISCORD_INVITE
|
||||
|| 'https://discordapp.com/';
|
||||
export const GUILDED_INVITE = process.env.GUILDED_INVITE
|
||||
|| 'https://www.guilded.gg/';
|
||||
|
||||
|
@ -87,13 +88,9 @@ export const auth = {
|
|||
},
|
||||
};
|
||||
|
||||
// o: none
|
||||
// 1: reCaptcha
|
||||
// 2: hCaptcha
|
||||
export const CAPTCHA_METHOD = Number(process.env.CAPTCHA_METHOD || 0);
|
||||
export const CAPTCHA_SECRET = process.env.CAPTCHA_SECRET || false;
|
||||
export const CAPTCHA_SITEKEY = process.env.CAPTCHA_SITEKEY || false;
|
||||
// time on which to display captcha in minutes
|
||||
export const CAPTCHA_TIME = parseInt(process.env.CAPTCHA_TIME, 10) || 30;
|
||||
// time during which the user can solve a captcha in seconds
|
||||
export const CAPTCHA_TIMEOUT = parseInt(process.env.CAPTCHA_TIMEOUT, 10) || 120;
|
||||
|
||||
export const SESSION_SECRET = process.env.SESSION_SECRET || 'dummy';
|
||||
|
|
51
src/reducers/alert.js
Normal file
51
src/reducers/alert.js
Normal 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;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ import canvas from './canvas';
|
|||
import gui from './gui';
|
||||
import modal from './modal';
|
||||
import user from './user';
|
||||
import ranks from './ranks';
|
||||
import alert from './alert';
|
||||
import chat from './chat';
|
||||
import contextMenu from './contextMenu';
|
||||
import chatRead from './chatRead';
|
||||
|
@ -17,6 +19,8 @@ import type { CanvasState } from './canvas';
|
|||
import type { GUIState } from './gui';
|
||||
import type { ModalState } from './modal';
|
||||
import type { UserState } from './user';
|
||||
import type { RanksState } from './ranks';
|
||||
import type { AlertState } from './alert';
|
||||
import type { ChatState } from './chat';
|
||||
import type { ContextMenuState } from './contextMenu';
|
||||
import type { FetchingState } from './fetching';
|
||||
|
@ -27,6 +31,8 @@ export type State = {
|
|||
gui: GUIState,
|
||||
modal: ModalState,
|
||||
user: UserState,
|
||||
ranks: RanksState,
|
||||
alert: AlertState,
|
||||
chat: ChatState,
|
||||
contextMenu: ContextMenuState,
|
||||
chatRead: ChatReadState,
|
||||
|
@ -38,7 +44,9 @@ const config = {
|
|||
storage: localForage,
|
||||
blacklist: [
|
||||
'user',
|
||||
'ranks',
|
||||
'canvas',
|
||||
'alert',
|
||||
'modal',
|
||||
'chat',
|
||||
'contextMenu',
|
||||
|
@ -52,6 +60,8 @@ export default persistCombineReducers(config, {
|
|||
gui,
|
||||
modal,
|
||||
user,
|
||||
ranks,
|
||||
alert,
|
||||
chat,
|
||||
contextMenu,
|
||||
chatRead,
|
||||
|
|
80
src/reducers/ranks.js
Normal file
80
src/reducers/ranks.js
Normal 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;
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import type { Action } from '../actions/types';
|
|||
import { createNameRegExp } from '../core/utils';
|
||||
|
||||
|
||||
|
||||
export type UserState = {
|
||||
name: string,
|
||||
center: Cell,
|
||||
|
@ -13,18 +12,9 @@ export type UserState = {
|
|||
coolDown: ?number, // ms
|
||||
lastCoolDownEnd: ?Date,
|
||||
requestingPixel: boolean,
|
||||
online: ?number,
|
||||
// messages are sent by api/me, like not_verified status
|
||||
messages: Array,
|
||||
mailreg: boolean,
|
||||
// stats
|
||||
totalPixels: number,
|
||||
dailyTotalPixels: number,
|
||||
ranking: number,
|
||||
dailyRanking: number,
|
||||
// global stats
|
||||
totalRanking: Object,
|
||||
totalDailyRanking: Object,
|
||||
// minecraft
|
||||
minecraftname: string,
|
||||
// blocking all Dms
|
||||
|
@ -46,11 +36,8 @@ const initialState: UserState = {
|
|||
coolDown: null,
|
||||
lastCoolDownEnd: null,
|
||||
requestingPixel: true,
|
||||
online: null,
|
||||
messages: [],
|
||||
mailreg: false,
|
||||
totalRanking: {},
|
||||
totalDailyRanking: {},
|
||||
minecraftname: null,
|
||||
blockDm: false,
|
||||
isOnMobile: false,
|
||||
|
@ -81,7 +68,7 @@ export default function user(
|
|||
};
|
||||
}
|
||||
|
||||
case 'SET_PLACE_ALLOWED': {
|
||||
case 'SET_REQUESTING_PIXEL': {
|
||||
const { requestingPixel } = action;
|
||||
return {
|
||||
...state,
|
||||
|
@ -120,35 +107,11 @@ export default function user(
|
|||
};
|
||||
}
|
||||
|
||||
case 'PLACED_PIXELS': {
|
||||
let { totalPixels, dailyTotalPixels } = state;
|
||||
const { amount } = action;
|
||||
totalPixels += amount;
|
||||
dailyTotalPixels += amount;
|
||||
return {
|
||||
...state,
|
||||
totalPixels,
|
||||
dailyTotalPixels,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_ONLINE': {
|
||||
const { online } = action;
|
||||
return {
|
||||
...state,
|
||||
online,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_ME':
|
||||
case 'LOGIN': {
|
||||
const {
|
||||
name,
|
||||
mailreg,
|
||||
totalPixels,
|
||||
dailyTotalPixels,
|
||||
ranking,
|
||||
dailyRanking,
|
||||
minecraftname,
|
||||
blockDm,
|
||||
userlvl,
|
||||
|
@ -160,10 +123,6 @@ export default function user(
|
|||
name,
|
||||
messages,
|
||||
mailreg,
|
||||
totalPixels,
|
||||
dailyTotalPixels,
|
||||
ranking,
|
||||
dailyRanking,
|
||||
minecraftname,
|
||||
blockDm,
|
||||
userlvl,
|
||||
|
@ -184,15 +143,6 @@ export default function user(
|
|||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_STATS': {
|
||||
const { totalRanking, totalDailyRanking } = action;
|
||||
return {
|
||||
...state,
|
||||
totalRanking,
|
||||
totalDailyRanking,
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_NAME': {
|
||||
const { name } = action;
|
||||
const nameRegExp = createNameRegExp(name);
|
||||
|
|
|
@ -8,42 +8,51 @@
|
|||
import type { Request, Response } from 'express';
|
||||
|
||||
import logger from '../../core/logger';
|
||||
import { verifyCaptcha } from '../../utils/captcha';
|
||||
import { checkCaptchaSolution } from '../../utils/captcha';
|
||||
import { getIPFromRequest } from '../../utils/ip';
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ip = getIPFromRequest(req);
|
||||
const { t } = req.ttag;
|
||||
|
||||
try {
|
||||
const { token } = req.body;
|
||||
if (!token) {
|
||||
const { text } = req.body;
|
||||
if (!text) {
|
||||
res.status(400)
|
||||
.json({ errors: [{ msg: 'No token given' }] });
|
||||
.json({ errors: [t`No captcha text given`] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await verifyCaptcha(token, ip)) {
|
||||
logger.info(`CAPTCHA ${ip} failed his captcha`);
|
||||
res.status(422)
|
||||
.json({
|
||||
errors: [{
|
||||
msg:
|
||||
'You failed your captcha',
|
||||
}],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const ret = await checkCaptchaSolution(text, ip);
|
||||
|
||||
res.status(200)
|
||||
.json({ success: true });
|
||||
switch (ret) {
|
||||
case 0:
|
||||
res.status(200)
|
||||
.json({ success: true });
|
||||
break;
|
||||
case 1:
|
||||
res.status(422)
|
||||
.json({
|
||||
errors: [t`You took too long, try again.`],
|
||||
});
|
||||
break;
|
||||
case 2:
|
||||
res.status(422)
|
||||
.json({
|
||||
errors: [t`You failed your captcha`],
|
||||
});
|
||||
break;
|
||||
default:
|
||||
res.status(422)
|
||||
.json({
|
||||
errors: [t`Unknown Captcha Error`],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('checkHuman', error);
|
||||
logger.error('CAPTCHA', error);
|
||||
res.status(500)
|
||||
.json({
|
||||
errors: [{
|
||||
msg:
|
||||
'Server error occured',
|
||||
}],
|
||||
errors: [t`Server error occured`],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -44,6 +44,11 @@ router.use((err, req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* make localisations available
|
||||
*/
|
||||
router.use(expressTTag);
|
||||
|
||||
// captcah doesn't need a user
|
||||
router.post('/captcha', captcha);
|
||||
|
||||
|
@ -89,11 +94,6 @@ router.post('/block', block);
|
|||
|
||||
router.post('/blockdm', blockdm);
|
||||
|
||||
/*
|
||||
* make localisations available
|
||||
*/
|
||||
router.use(expressTTag);
|
||||
|
||||
router.get('/chathistory', chatHistory);
|
||||
|
||||
router.get('/me', me);
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
/* eslint-disable max-len */
|
||||
|
||||
import React from 'react';
|
||||
import { CAPTCHA_METHOD, CAPTCHA_SITEKEY } from '../core/config';
|
||||
|
||||
const Html = ({
|
||||
title,
|
||||
|
@ -25,8 +24,6 @@ const Html = ({
|
|||
styles,
|
||||
// code as string
|
||||
code,
|
||||
// if recaptcha should get loaded
|
||||
useCaptcha,
|
||||
}) => (
|
||||
<html className="no-js" lang="en">
|
||||
<head>
|
||||
|
@ -48,10 +45,6 @@ const Html = ({
|
|||
dangerouslySetInnerHTML={{ __html: style.cssText }}
|
||||
/>
|
||||
))}
|
||||
{(CAPTCHA_METHOD === 1) && CAPTCHA_SITEKEY && useCaptcha
|
||||
&& <script src="https://www.google.com/recaptcha/api.js" async defer />}
|
||||
{(CAPTCHA_METHOD === 2) && CAPTCHA_SITEKEY && useCaptcha
|
||||
&& <script src="https://hcaptcha.com/1/api.js" async defer />}
|
||||
{code && (
|
||||
<script
|
||||
// eslint-disable-next-line react/no-danger
|
||||
|
@ -67,24 +60,6 @@ const Html = ({
|
|||
{body}
|
||||
</div>
|
||||
{scripts && scripts.map((script) => <script key={script} src={script} />)}
|
||||
{(CAPTCHA_METHOD === 2) && CAPTCHA_SITEKEY && useCaptcha
|
||||
&& (
|
||||
<div
|
||||
className="h-captcha"
|
||||
data-sitekey={CAPTCHA_SITEKEY}
|
||||
data-callback="onCaptcha"
|
||||
data-size="invisible"
|
||||
/>
|
||||
)}
|
||||
{(CAPTCHA_METHOD === 1) && CAPTCHA_SITEKEY && useCaptcha
|
||||
&& (
|
||||
<div
|
||||
className="g-recaptcha"
|
||||
data-sitekey={CAPTCHA_SITEKEY}
|
||||
data-callback="onCaptcha"
|
||||
data-size="invisible"
|
||||
/>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ import assets from './assets.json';
|
|||
// eslint-disable-next-line import/no-unresolved
|
||||
import styleassets from './styleassets.json';
|
||||
|
||||
import { ASSET_SERVER, BACKUP_URL } from '../core/config';
|
||||
import { CAPTCHA_URL, ASSET_SERVER, BACKUP_URL } from '../core/config';
|
||||
|
||||
/*
|
||||
* generate language list
|
||||
|
@ -31,6 +31,7 @@ const langs = Object.keys(ttags)
|
|||
*/
|
||||
const ssv = {
|
||||
assetserver: ASSET_SERVER,
|
||||
captchaurl: CAPTCHA_URL,
|
||||
availableStyles: styleassets,
|
||||
langs,
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@ import thunk from 'redux-thunk';
|
|||
import { persistStore } from 'redux-persist';
|
||||
|
||||
import audio from './audio';
|
||||
import swal from './sweetAlert';
|
||||
import protocolClientHook from './protocolClientHook';
|
||||
import rendererHook from './rendererHook';
|
||||
// import ads from './ads';
|
||||
|
@ -13,6 +12,7 @@ import array from './array';
|
|||
import promise from './promise';
|
||||
import notifications from './notifications';
|
||||
import title from './title';
|
||||
import placePixelControl from './placePixelControl';
|
||||
import extensions from './extensions';
|
||||
import reducers from '../reducers';
|
||||
|
||||
|
@ -25,12 +25,12 @@ const store = createStore(
|
|||
thunk,
|
||||
promise,
|
||||
array,
|
||||
swal,
|
||||
audio,
|
||||
notifications,
|
||||
title,
|
||||
protocolClientHook,
|
||||
rendererHook,
|
||||
placePixelControl,
|
||||
extensions,
|
||||
),
|
||||
),
|
||||
|
|
20
src/store/placePixelControl.js
Normal file
20
src/store/placePixelControl.js
Normal 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);
|
||||
};
|
|
@ -47,7 +47,7 @@ export default (store) => (next) => (action) => {
|
|||
break;
|
||||
}
|
||||
|
||||
case 'SET_PLACE_ALLOWED': {
|
||||
case 'SET_REQUESTING_PIXEL': {
|
||||
const renderer = getRenderer();
|
||||
renderer.forceNextSubRender = true;
|
||||
break;
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -112,6 +112,8 @@ td, th {
|
|||
border: 1px solid #dddddd;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
max-width: 18em;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
|
@ -322,7 +324,7 @@ tr:nth-child(even) {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
.Modal, .Alert {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
@ -334,14 +336,26 @@ tr:nth-child(even) {
|
|||
border-radius: 4px;
|
||||
outline: currentcolor none medium;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: opacity 200ms ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
height: 80%;
|
||||
max-height: 900px;
|
||||
width: 70%;
|
||||
transition: all 0.5s ease 0s;
|
||||
max-height: 900px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.Alert {
|
||||
max-height: 100%;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.modaltext, .modalcotext {
|
||||
color: hsla(218, 5%, 47%, .6);
|
||||
color: hsla(0, 0%, 0%, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
|
@ -370,7 +384,7 @@ tr:nth-child(even) {
|
|||
.modaldesc {
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
color: hsla(218, 5%, 47%, .6);
|
||||
color: hsla(0, 0%, 0%, 0.6);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
|
@ -397,7 +411,7 @@ tr:nth-child(even) {
|
|||
}
|
||||
|
||||
.modalcvtext {
|
||||
color: hsla(218, 5%, 47%, .6);
|
||||
color: hsla(0, 0%, 0%, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
|
@ -432,7 +446,7 @@ tr:nth-child(even) {
|
|||
}
|
||||
|
||||
@media (max-width: 604px) {
|
||||
.Modal {
|
||||
.Modal, .Alert {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
|
@ -446,16 +460,26 @@ tr:nth-child(even) {
|
|||
padding: 5%;
|
||||
}
|
||||
}
|
||||
.Overlay {
|
||||
|
||||
.OverlayModal, .OverlayAlert {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
}
|
||||
|
||||
.OverlayModal {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.OverlayAlert {
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.chatbox div .chatarea {
|
||||
height: 174px;
|
||||
}
|
||||
|
@ -697,15 +721,6 @@ tr:nth-child(even) {
|
|||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ReactModal__Overlay {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
}
|
||||
|
||||
.ReactModal__Overlay--after-open{
|
||||
.Modal.show, .Alert.show, .OverlayAlert.show, .OverlayModal.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ReactModal__Overlay--before-close{
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ tr:nth-child(odd) {
|
|||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
.OverlayModal, .OverlayAlert {
|
||||
background: linear-gradient(175deg, #61dceaab , #ffb1e1, #ecffec, #ffb1e1, #61dceaab);
|
||||
}
|
||||
|
||||
|
@ -54,3 +54,8 @@ tr:nth-child(odd) {
|
|||
background-size: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.Alert {
|
||||
background: #f4edf0 none repeat scroll 0 0;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
|
|
@ -65,12 +65,19 @@ tr:nth-child(even) {
|
|||
background-color: #15374fd1;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
.Modal, .Alert {
|
||||
background: #444242 none repeat scroll 0 0;;
|
||||
color: #f4f4f4;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
border-radius: 21px;
|
||||
}
|
||||
|
||||
.Alert {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.modaltext, .modalcotext {
|
||||
color: #f4f4f4;
|
||||
}
|
||||
|
@ -104,7 +111,7 @@ tr:nth-child(even) {
|
|||
background-color: #6f6f75;
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
.OverlayModal, .OverlayAlert {
|
||||
background-color: rgba(187, 187, 187, 0.75);
|
||||
}
|
||||
|
||||
|
|
149
src/styles/theme-dark-sstraight.css
Normal file
149
src/styles/theme-dark-sstraight.css
Normal 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;
|
||||
}
|
|
@ -44,7 +44,7 @@ tr:nth-child(even) {
|
|||
background-color: #15374fd1;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
.Modal, .Alert {
|
||||
background: #444242 none repeat scroll 0 0;;
|
||||
color: #f4f4f4;
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ tr:nth-child(even) {
|
|||
background-color: #6f6f75;
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
.OverlayModal, .OverlayAlert {
|
||||
background-color: rgba(187, 187, 187, 0.75);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
border-radius: 21px;
|
||||
}
|
||||
|
||||
.Alert {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.notifybox {
|
||||
border-radius: 21px;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { t } from 'ttag';
|
||||
import {
|
||||
notify,
|
||||
setPlaceAllowed,
|
||||
setRequestingPixel,
|
||||
sweetAlert,
|
||||
gotCoolDownDelta,
|
||||
pixelFailure,
|
||||
|
@ -38,7 +38,7 @@ let clientPredictions = [];
|
|||
let lastRequestValues = {};
|
||||
|
||||
|
||||
function requestFromQueue(store) {
|
||||
export function requestFromQueue(store) {
|
||||
if (!pixelQueue.length) {
|
||||
pixelTimeout = null;
|
||||
return;
|
||||
|
@ -48,7 +48,7 @@ function requestFromQueue(store) {
|
|||
pixelTimeout = setTimeout(() => {
|
||||
pixelQueue = [];
|
||||
pixelTimeout = null;
|
||||
store.dispatch(setPlaceAllowed(true));
|
||||
store.dispatch(setRequestingPixel(true));
|
||||
store.dispatch(sweetAlert(
|
||||
t`Error :(`,
|
||||
t`Didn't get an answer from pixelplanet. Maybe try to refresh?`,
|
||||
|
@ -60,16 +60,7 @@ function requestFromQueue(store) {
|
|||
lastRequestValues = pixelQueue.shift();
|
||||
const { i, j, pixels } = lastRequestValues;
|
||||
ProtocolClient.requestPlacePixels(i, j, pixels);
|
||||
store.dispatch(setPlaceAllowed(false));
|
||||
|
||||
// TODO:
|
||||
// this is for resending after captcha returned
|
||||
// window is ugly, put it into redux or something
|
||||
window.pixel = {
|
||||
i,
|
||||
j,
|
||||
pixels,
|
||||
};
|
||||
store.dispatch(setRequestingPixel(false));
|
||||
}
|
||||
|
||||
export function receivePixelUpdate(
|
||||
|
@ -239,15 +230,15 @@ export function receivePixelReturn(
|
|||
store.dispatch(pixelWait());
|
||||
break;
|
||||
case 10:
|
||||
// captcha, reCaptcha or hCaptcha
|
||||
if (typeof window.hcaptcha !== 'undefined') {
|
||||
window.hcaptcha.execute();
|
||||
} else {
|
||||
window.grecaptcha.execute();
|
||||
}
|
||||
store.dispatch(sweetAlert(
|
||||
'Captcha',
|
||||
t`Please prove that you are human`,
|
||||
'captcha',
|
||||
t`OK`,
|
||||
));
|
||||
store.dispatch(setRequestingPixel(true));
|
||||
return;
|
||||
case 11:
|
||||
|
||||
errorTitle = t`No Proxies Allowed :(`;
|
||||
msg = t`You are using a Proxy.`;
|
||||
break;
|
||||
|
@ -266,7 +257,7 @@ export function receivePixelReturn(
|
|||
));
|
||||
}
|
||||
|
||||
store.dispatch(setPlaceAllowed(true));
|
||||
store.dispatch(setRequestingPixel(true));
|
||||
/* start next request if queue isn't empty */
|
||||
requestFromQueue(store);
|
||||
}
|
||||
|
|
|
@ -3,117 +3,70 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import logger from '../core/logger';
|
||||
import redis from '../data/redis';
|
||||
|
||||
import { getIPv6Subnet } from './ip';
|
||||
import {
|
||||
CAPTCHA_METHOD,
|
||||
CAPTCHA_SECRET,
|
||||
CAPTCHA_URL,
|
||||
CAPTCHA_TIME,
|
||||
CAPTCHA_TIMEOUT,
|
||||
} from '../core/config';
|
||||
|
||||
const TTL_CACHE = CAPTCHA_TIME * 60; // seconds
|
||||
// eslint-disable-next-line max-len
|
||||
const RECAPTCHA_ENDPOINT = `https://www.google.com/recaptcha/api/siteverify?secret=${CAPTCHA_SECRET}`;
|
||||
const HCAPTCHA_ENDPOINT = 'https://hcaptcha.com/siteverify';
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/27297067/google-recaptcha-how-to-get-user-response-and-validate-in-the-server-side
|
||||
*
|
||||
* @param token
|
||||
* @param ip
|
||||
* @returns {Promise.<boolean>}
|
||||
*/
|
||||
async function verifyReCaptcha(
|
||||
token: string,
|
||||
ip: string,
|
||||
): Promise<boolean> {
|
||||
const url = `${RECAPTCHA_ENDPOINT}&response=${token}&remoteip=${ip}`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const { success } = await response.json();
|
||||
if (success) {
|
||||
logger.info(`CAPTCHA ${ip} successfully solved captcha`);
|
||||
return true;
|
||||
}
|
||||
logger.info(`CAPTCHA Token for ${ip} not ok`);
|
||||
} else {
|
||||
logger.warn(`CAPTCHA Recapcha answer for ${ip} not ok`);
|
||||
}
|
||||
return false;
|
||||
function captchaTextFilter(text: string) {
|
||||
let ret = text.toString('utf8');
|
||||
ret = ret.split('l').join('i');
|
||||
ret = ret.split('0').join('O');
|
||||
ret = ret.toLowerCase();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* https://docs.hcaptcha.com/
|
||||
* set captcha solution
|
||||
*
|
||||
* @param token
|
||||
* @param text Solution of captcha
|
||||
* @param ip
|
||||
* @return boolean, true if successful, false on error or fail
|
||||
* @param ttl time to be valid in seconds
|
||||
*/
|
||||
async function verifyHCaptcha(
|
||||
token: string,
|
||||
export function setCaptchaSolution(
|
||||
text: string,
|
||||
ip: string,
|
||||
): Promise<boolean> {
|
||||
const response = await fetch(HCAPTCHA_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `response=${token}&secret=${CAPTCHA_SECRET}&remoteip=${ip}`,
|
||||
});
|
||||
if (response.ok) {
|
||||
const { success } = await response.json();
|
||||
if (success) {
|
||||
logger.info(`CAPTCHA ${ip} successfully solved captcha`);
|
||||
return true;
|
||||
}
|
||||
logger.info(`CAPTCHA Token for ${ip} not ok`);
|
||||
} else {
|
||||
// eslint-disable-next-line max-len
|
||||
logger.warn(`CAPTCHA hCapcha answer for ${ip} not ok ${await response.text()}`);
|
||||
}
|
||||
return false;
|
||||
) {
|
||||
const key = `capt:${ip}`;
|
||||
return redis.setAsync(key, captchaTextFilter(text), 'EX', CAPTCHA_TIMEOUT);
|
||||
}
|
||||
|
||||
/*
|
||||
* verify captcha token from client
|
||||
* check captcha solution
|
||||
*
|
||||
* @param token token of solved captcha from client
|
||||
* @param text Solution of captcha
|
||||
* @param ip
|
||||
* @returns Boolean if successful
|
||||
* @return 0 if solution right
|
||||
* 1 if timed out
|
||||
* 2 if wrong
|
||||
*/
|
||||
export async function verifyCaptcha(
|
||||
token: string,
|
||||
export async function checkCaptchaSolution(
|
||||
text: string,
|
||||
ip: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!CAPTCHA_METHOD) {
|
||||
return true;
|
||||
) {
|
||||
const ipn = getIPv6Subnet(ip);
|
||||
const key = `capt:${ip}`;
|
||||
let solution = await redis.getAsync(key);
|
||||
if (solution) {
|
||||
if (solution.toString('utf8') === captchaTextFilter(text)) {
|
||||
const solvkey = `human:${ipn}`;
|
||||
await redis.setAsync(solvkey, '', 'EX', TTL_CACHE);
|
||||
logger.info(`CAPTCHA ${ip} successfully solved captcha`);
|
||||
return 0;
|
||||
}
|
||||
const key = `human:${ip}`;
|
||||
|
||||
switch (CAPTCHA_METHOD) {
|
||||
case 1:
|
||||
if (!await verifyReCaptcha(token, ip)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (!await verifyHCaptcha(token, ip)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// nothing
|
||||
}
|
||||
|
||||
await redis.setAsync(key, '', 'EX', TTL_CACHE);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
logger.info(
|
||||
`CAPTCHA ${ip} got captcha wrong (${text} instead of ${solution})`,
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
return false;
|
||||
logger.info(`CAPTCHA ${ip} timed out`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -123,11 +76,11 @@ export async function verifyCaptcha(
|
|||
* @return boolean true if needed
|
||||
*/
|
||||
export async function needCaptcha(ip: string) {
|
||||
if (!CAPTCHA_METHOD) {
|
||||
if (!CAPTCHA_URL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = `human:${ip}`;
|
||||
const key = `human:${getIPv6Subnet(ip)}`;
|
||||
const ttl: number = await redis.ttlAsync(key);
|
||||
if (ttl > 0) {
|
||||
return false;
|
||||
|
@ -135,6 +88,3 @@ export async function needCaptcha(ip: string) {
|
|||
logger.info(`CAPTCHA ${ip} got captcha`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export default verifyCaptcha;
|
||||
|
|
|
@ -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;
|
|
@ -3,20 +3,11 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import isCloudflareIp from './cloudflareip';
|
||||
|
||||
import logger from '../core/logger';
|
||||
|
||||
import { USE_XREALIP } from '../core/config';
|
||||
|
||||
|
||||
function isTrustedProxy(ip: string): boolean {
|
||||
if (ip === '::ffff:127.0.0.1' || ip === '127.0.0.1' || isCloudflareIp(ip)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getHostFromRequest(req): ?string {
|
||||
const { headers } = req;
|
||||
const host = headers['x-forwarded-host'] || headers.host;
|
||||
|
@ -26,30 +17,25 @@ export function getHostFromRequest(req): ?string {
|
|||
}
|
||||
|
||||
export function getIPFromRequest(req): ?string {
|
||||
const { socket, connection, headers } = req;
|
||||
if (USE_XREALIP) {
|
||||
const ip = req.headers['x-real-ip'];
|
||||
if (ip) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
const { socket, connection } = req;
|
||||
|
||||
let conip = (connection ? connection.remoteAddress : socket.remoteAddress);
|
||||
conip = conip || '0.0.0.1';
|
||||
|
||||
if (USE_XREALIP) {
|
||||
const ip = headers['x-real-ip'];
|
||||
return ip || conip;
|
||||
if (!USE_XREALIP) {
|
||||
logger.warn(
|
||||
`Connection not going through reverse proxy! IP: ${conip}`, req.headers,
|
||||
);
|
||||
}
|
||||
|
||||
if (!headers['x-forwarded-for'] || !isTrustedProxy(conip)) {
|
||||
// eslint-disable-next-line max-len
|
||||
logger.warn(`Connection not going through nginx and cloudflare! IP: ${conip}`, headers);
|
||||
return conip;
|
||||
}
|
||||
|
||||
const forwardedFor = headers['x-forwarded-for'];
|
||||
const ipList = forwardedFor.split(',').map((str) => str.trim());
|
||||
|
||||
let ip = ipList.pop();
|
||||
while (isTrustedProxy(ip) && ipList.length) {
|
||||
ip = ipList.pop();
|
||||
}
|
||||
return ip || conip;
|
||||
return conip;
|
||||
}
|
||||
|
||||
export function getIPv6Subnet(ip: string): string {
|
||||
|
|
|
@ -61,23 +61,3 @@ export function validatePassword(password) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* makes sure that responses from the api
|
||||
* includes errors when failure occures
|
||||
*/
|
||||
export async function parseAPIresponse(response) {
|
||||
try {
|
||||
const resp = await response.json();
|
||||
if (!response.ok && !resp.errors) {
|
||||
return {
|
||||
errors: [t`Could not connect to server, please try again later :(`],
|
||||
};
|
||||
}
|
||||
return resp;
|
||||
} catch (e) {
|
||||
return {
|
||||
errors: [t`I think we experienced some error :(`],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
14
src/web.js
14
src/web.js
|
@ -31,7 +31,7 @@ import generateGlobePage from './ssr-components/Globe';
|
|||
import generateMainPage from './ssr-components/Main';
|
||||
|
||||
import { SECOND, MONTH } from './core/constants';
|
||||
import { PORT, DISCORD_INVITE, GUILDED_INVITE } from './core/config';
|
||||
import { PORT, HOST, GUILDED_INVITE } from './core/config';
|
||||
|
||||
import { ccToCoords } from './utils/location';
|
||||
import { startAllCanvasLoops } from './core/tileserver';
|
||||
|
@ -111,11 +111,8 @@ app.use(express.static(path.join(__dirname, 'public'), {
|
|||
|
||||
|
||||
//
|
||||
// Redirecct to discord and guilded
|
||||
// Redirecct to guilded
|
||||
// -----------------------------------------------------------------------------
|
||||
app.use('/discord', (req, res) => {
|
||||
res.redirect(DISCORD_INVITE);
|
||||
});
|
||||
app.use('/guilded', (req, res) => {
|
||||
res.redirect(GUILDED_INVITE);
|
||||
});
|
||||
|
@ -203,10 +200,13 @@ const promise = models.sync({ alter: { drop: false } })
|
|||
// const promise = models.sync()
|
||||
.catch((err) => logger.error(err.stack));
|
||||
promise.then(() => {
|
||||
server.listen(PORT, () => {
|
||||
server.listen(PORT, HOST, () => {
|
||||
rankings.updateRanking();
|
||||
chatProvider.initialize();
|
||||
const address = server.address();
|
||||
logger.log('info', `web is running at http://localhost:${address.port}/`);
|
||||
logger.log(
|
||||
'info',
|
||||
`web is running at http://${address.host}:${address.port}/`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -64,6 +64,7 @@ export default ({
|
|||
entry: {
|
||||
web: [path.resolve(__dirname, 'src', 'web.js')],
|
||||
backup: [path.resolve(__dirname, 'src', 'backup.js')],
|
||||
captchaserver: [path.resolve(__dirname, 'src', 'captchaserver.js')],
|
||||
},
|
||||
|
||||
output: {
|
||||
|
|
Loading…
Reference in New Issue
Block a user