From 03e80bdff3aaa8a2098820d59234da91afd03033 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sat, 13 Jul 2019 19:15:35 +0000 Subject: [PATCH] feat: legacy login --- client/scss/legacy.scss | 116 ++++++++++++++++++++++++++++++++- dev/templates/legacy.pug | 53 +-------------- dev/webpack/webpack.dev.js | 2 +- dev/webpack/webpack.prod.js | 2 +- package.json | 1 + server/controllers/auth.js | 88 ++++++++++++++++++++++++- server/controllers/common.js | 2 +- server/views/legacy/login.pug | 21 ++++++ server/views/legacy/master.pug | 82 +++++++++++++++++++++++ server/views/legacy/page.pug | 56 ++++++++++++++++ server/views/master.pug | 20 ++++++ yarn.lock | 12 +++- 12 files changed, 395 insertions(+), 60 deletions(-) create mode 100644 server/views/legacy/login.pug create mode 100644 server/views/legacy/master.pug create mode 100644 server/views/legacy/page.pug diff --git a/client/scss/legacy.scss b/client/scss/legacy.scss index 1290ef4e..7b62b735 100644 --- a/client/scss/legacy.scss +++ b/client/scss/legacy.scss @@ -28,6 +28,120 @@ body { min-height: 100vh; } +// LOGIN + +.login { + background-color: mc('grey', '900'); + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + + &-deprecated { + position: absolute; + top: 0; + left: 0; + width: 100%; + background-color: mc('red', '700'); + text-align: center; + color: mc('red', '50'); + height: 64px; + display: flex; + align-items: center; + justify-content: center; + + a { + color: #FFF; + margin-left: 5px; + } + } + + &-dialog { + width: 650px; + background-color: mc('grey', '100'); + border-radius: 5px; + text-align: center; + padding: 2rem; + color: mc('grey', '800'); + + h1 { + margin-bottom: 2rem; + } + + input, select { + display: block; + background-color: #FFF; + border: none; + border-radius: 5px; + width: 100%; + height: 40px; + padding: 0 1rem; + margin: 5px 0; + } + + button { + height: 40px; + display: block; + width: 200px; + border: none; + border-radius: 5px; + margin: 0 auto; + background-color: mc('blue', '700'); + color: #FFF; + cursor: pointer; + margin-top: 1rem; + font-weight: 600; + + &:hover { + background-color: mc('blue', '800'); + } + } + } + + &-social { + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid mc('grey', '400'); + + h2 { + font-size: 14px; + font-weight: 600; + margin-bottom: 1rem; + } + + &-icon { + display: inline-flex; + justify-content: center; + align-items: center; + border-radius: 5px; + width: 54px; + height: 54px; + cursor: pointer; + transition: opacity .2s ease; + margin: .5rem .25rem; + &:hover { + opacity: .8; + } + svg { + width: 24px; + height: 24px; + bottom: 0; + path { + fill: #FFF; + } + } + + @each $colorName, $color in $material-colors { + &.#{$colorName} { + background-color: map-get($color, '500'); + } + } + } + } +} + +// PAGE + .header { background-color: #000; color: #FFF; @@ -145,7 +259,7 @@ body { } &-right { - flex: 0 0 324px; + flex: 0 0 308px; padding-left: 16px; &-title { diff --git a/dev/templates/legacy.pug b/dev/templates/legacy.pug index 4248c7b6..8c3e964f 100644 --- a/dev/templates/legacy.pug +++ b/dev/templates/legacy.pug @@ -90,58 +90,9 @@ html != analyticsCode.head - if injectCode.css - style(type='text/css')!= injectCode.css - if injectCode.head - != injectCode.head + block head body != analyticsCode.bodyStart - #root - .header - span.header-title= siteConfig.title - span.header-deprecated Your browser is outdated. Upgrade to a #[a(href='https://bestvpn.org/outdatedbrowser/en', rel='nofollow') modern browser]. - span.header-login - a(href='/login') - i.material-icons account_circle - .main - .sidebar - each navItem in sidebar - if navItem.kind === 'link' - a.sidebar-link(href=navItem.target) - i.material-icons= navItem.icon - span= navItem.label - else if navItem.kind === 'divider' - .sidebar-divider - else if navItem.kind === 'header' - .sidebar-title= navItem.label - .main-container - .page-header - .page-header-left - h1= page.title - h2= page.description - .page-header-right - .page-header-right-title Last edited by - .page-header-right-author= page.authorName - .page-header-right-updated= page.updatedAt - .page-contents - .contents - div!= page.render - if page.toc.length - .toc - .toc-title Table of Contents - each tocItem, tocIdx in page.toc - a.toc-tile(href='#' + tocItem.anchor) - i.material-icons arrow_right - span= tocItem.title - if tocIdx < page.toc.length - 1 || tocItem.children.length - .toc-divider - each tocSubItem in tocItem.children - a.toc-tile.inset(href='#' + tocSubItem.anchor) - i.material-icons arrow_right - span= tocSubItem.title - if tocIdx < page.toc.length - 1 - .toc-divider.inset - if injectCode.body - != injectCode.body + block body != analyticsCode.bodyEnd diff --git a/dev/webpack/webpack.dev.js b/dev/webpack/webpack.dev.js index 375e10bd..92283ffe 100644 --- a/dev/webpack/webpack.dev.js +++ b/dev/webpack/webpack.dev.js @@ -199,7 +199,7 @@ module.exports = { }), new HtmlWebpackPlugin({ template: 'dev/templates/legacy.pug', - filename: '../server/views/legacy.pug', + filename: '../server/views/legacy/master.pug', hash: false, inject: false, excludeChunks: ['setup', 'app'] diff --git a/dev/webpack/webpack.prod.js b/dev/webpack/webpack.prod.js index 8bd9f17b..19498ff9 100644 --- a/dev/webpack/webpack.prod.js +++ b/dev/webpack/webpack.prod.js @@ -209,7 +209,7 @@ module.exports = { }), new HtmlWebpackPlugin({ template: 'dev/templates/legacy.pug', - filename: '../server/views/legacy.pug', + filename: '../server/views/legacy/master.pug', hash: false, inject: false, excludeChunks: ['setup', 'app'] diff --git a/package.json b/package.json index 14b6d688..fa919f5d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "bcryptjs-then": "1.0.1", "bluebird": "3.5.5", "body-parser": "1.19.0", + "brute-knex": "4.0.0", "chalk": "2.4.2", "cheerio": "1.0.0-rc.3", "chokidar": "3.0.1", diff --git a/server/controllers/auth.js b/server/controllers/auth.js index 93d8b25a..9eaa7ff9 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -1,17 +1,70 @@ /* global WIKI */ const express = require('express') +const ExpressBrute = require('express-brute') +const BruteKnex = require('brute-knex') const router = express.Router() const moment = require('moment') const _ = require('lodash') +const fs = require('fs-extra') +const path = require('path') + +const bruteforce = new ExpressBrute(new BruteKnex({ + createTable: true, + knex: WIKI.models.knex +}), { + freeRetries: 5, + minWait: 5*60*1000, // 5 minutes + maxWait: 60*60*1000, // 1 hour + failCallback: (req, res, next) => { + res.status(401).send('Too many failed attempts. Try again later.') + } +}) /** * Login form */ -router.get('/login', (req, res, next) => { +router.get('/login', async (req, res, next) => { _.set(res.locals, 'pageMeta.title', 'Login') - res.render('login') + + if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) { + const strategies = await WIKI.models.authentication.query().select('key', 'selfRegistration').where({ isEnabled: true }) + let formStrategies = [] + let socialStrategies = [] + + // TODO: Let's refactor that at some point... + for (let stg of strategies) { + const stgInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {} + if (stgInfo.useForm) { + formStrategies.push({ + key: stg.key, + title: stgInfo.title + }) + } else { + socialStrategies.push({ + ...stgInfo, + ...stg, + icon: await fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${stg.key}.svg`), 'utf8').catch(err => { + if (err.code === 'ENOENT') { + return null + } + throw err + }) + }) + } + } + res.render('legacy/login', { + formStrategies, + socialStrategies + }) + } else { + res.render('login') + } }) + +/** + * Social Strategies Login + */ router.get('/login/:strategy', async (req, res, next) => { try { await WIKI.models.users.login({ @@ -21,6 +74,10 @@ router.get('/login/:strategy', async (req, res, next) => { next(err) } }) + +/** + * Social Strategies Callback + */ router.all('/login/:strategy/callback', async (req, res, next) => { if (req.method !== 'GET' && req.method !== 'POST') { return next() } @@ -35,6 +92,30 @@ router.all('/login/:strategy/callback', async (req, res, next) => { } }) +/** + * LEGACY - Login form handling + */ +router.post('/login', bruteforce.prevent, async (req, res, next) => { + _.set(res.locals, 'pageMeta.title', 'Login') + + if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) { + try { + const authResult = await WIKI.models.users.login({ + strategy: req.body.strategy, + username: req.body.user, + password: req.body.pass + }, { req, res }) + req.brute.reset() + res.cookie('jwt', authResult.jwt, { expires: moment().add(1, 'y').toDate() }) + res.redirect('/') + } catch (err) { + res.render('legacy/login') + } + } else { + res.redirect('/login') + } +}) + /** * Logout */ @@ -59,10 +140,11 @@ router.get('/register', async (req, res, next) => { /** * Verify */ -router.get('/verify/:token', async (req, res, next) => { +router.get('/verify/:token', bruteforce.prevent, async (req, res, next) => { const usr = await WIKI.models.userKeys.validateToken({ kind: 'verify', token: req.params.token }) await WIKI.models.users.query().patch({ isVerified: true }).where('id', usr.id) const result = await WIKI.models.users.refreshToken(usr) + req.brute.reset() res.cookie('jwt', result.token, { expires: moment().add(1, 'years').toDate() }) res.redirect('/') }) diff --git a/server/controllers/common.js b/server/controllers/common.js index 8f52c3d9..5a891f64 100644 --- a/server/controllers/common.js +++ b/server/controllers/common.js @@ -200,7 +200,7 @@ router.get('/*', async (req, res, next) => { if (_.isString(page.toc)) { page.toc = JSON.parse(page.toc) } - res.render('legacy', { page, sidebar, injectCode }) + res.render('legacy/page', { page, sidebar, injectCode }) } else { res.render('page', { page, sidebar, injectCode }) } diff --git a/server/views/legacy/login.pug b/server/views/legacy/login.pug new file mode 100644 index 00000000..de870b58 --- /dev/null +++ b/server/views/legacy/login.pug @@ -0,0 +1,21 @@ +extends master.pug + +block body + #root + .login-deprecated Your browser is outdated. Upgrade to a #[a(href='https://bestvpn.org/outdatedbrowser/en', rel='nofollow') modern browser]. + .login + .login-dialog + form(method='post', action='/login') + h1= config.title + select(name='strategy') + each str in formStrategies + option(value=str.key, selected)= str.title + input(type='text', name='user', placeholder='Username / Email') + input(type='password', name='pass', placeholder='Password') + button(type='submit') Login + if socialStrategies.length + .login-social + h2 or login using... + each str in socialStrategies + a.login-social-icon(href='/login/' + str.key, class=str.color) + != str.icon diff --git a/server/views/legacy/master.pug b/server/views/legacy/master.pug new file mode 100644 index 00000000..a89a8dc1 --- /dev/null +++ b/server/views/legacy/master.pug @@ -0,0 +1,82 @@ +doctype html +html + head + meta(http-equiv='X-UA-Compatible', content='IE=edge') + meta(charset='UTF-8') + meta(name='viewport', content='user-scalable=yes, width=device-width, initial-scale=1, maximum-scale=5') + meta(name='theme-color', content='#333333') + meta(name='msapplication-TileColor', content='#333333') + meta(name='msapplication-TileImage', content='/favicons/ms-icon-144x144.png') + + title= pageMeta.title + ' | ' + config.title + + //- SEO / OpenGraph + meta(name='description', content=pageMeta.description) + meta(property='og:title', content=pageMeta.title) + meta(property='og:type', content='website') + meta(property='og:description', content=pageMeta.description) + meta(property='og:image', content=pageMeta.image) + meta(property='og:url', content=pageMeta.url) + meta(property='og:site_name', content=config.title) + + //- Favicon + each favsize in [57, 60, 72, 76, 114, 120, 144, 152, 180] + link(rel='apple-touch-icon', sizes=favsize + 'x' + favsize, href='/favicons/apple-icon-' + favsize + 'x' + favsize + '.png') + link(rel='icon', type='image/png', sizes='192x192', href='/favicons/android-icon-192x192.png') + each favsize in [32, 96, 16] + link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize, href='/favicons/favicon-' + favsize + 'x' + favsize + '.png') + link(rel='manifest', href='/manifest.json') + + //- Icon Set + if config.theming.iconset === 'mdi' + link( + type='text/css' + rel='stylesheet' + href='https://cdn.materialdesignicons.com/3.7.95/css/materialdesignicons.min.css' + ) + else if config.theming.iconset === 'fa' + link( + type='text/css' + rel='stylesheet' + href='https://use.fontawesome.com/releases/v5.0.13/css/all.css' + ) + else if config.theming.iconset === 'fa4' + link( + type='text/css' + rel='stylesheet' + href='https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css' + ) + + //- CSS + + + script( + crossorigin='anonymous' + src='https://polyfill.io/v3/polyfill.min.js?features=EventSource' + ) + + //- JS + + + script( + type='text/javascript' + src='/js/runtime.js' + ) + + + + script( + type='text/javascript' + src='/js/legacy.js' + ) + + + + != analyticsCode.head + + block head + + body + != analyticsCode.bodyStart + block body + != analyticsCode.bodyEnd diff --git a/server/views/legacy/page.pug b/server/views/legacy/page.pug new file mode 100644 index 00000000..dcc58d53 --- /dev/null +++ b/server/views/legacy/page.pug @@ -0,0 +1,56 @@ +extends master.pug + +block head + if injectCode.css + style(type='text/css')!= injectCode.css + if injectCode.head + != injectCode.head + +block body + #root + .header + span.header-title= siteConfig.title + span.header-deprecated Your browser is outdated. Upgrade to a #[a(href='https://bestvpn.org/outdatedbrowser/en', rel='nofollow') modern browser]. + span.header-login + a(href='/login') + i.material-icons account_circle + .main + .sidebar + each navItem in sidebar + if navItem.kind === 'link' + a.sidebar-link(href=navItem.target) + i.material-icons= navItem.icon + span= navItem.label + else if navItem.kind === 'divider' + .sidebar-divider + else if navItem.kind === 'header' + .sidebar-title= navItem.label + .main-container + .page-header + .page-header-left + h1= page.title + h2= page.description + .page-header-right + .page-header-right-title Last edited by + .page-header-right-author= page.authorName + .page-header-right-updated= page.updatedAt + .page-contents + .contents + div!= page.render + if page.toc.length + .toc + .toc-title Table of Contents + each tocItem, tocIdx in page.toc + a.toc-tile(href='#' + tocItem.anchor) + i.material-icons arrow_right + span= tocItem.title + if tocIdx < page.toc.length - 1 || tocItem.children.length + .toc-divider + each tocSubItem in tocItem.children + a.toc-tile.inset(href='#' + tocSubItem.anchor) + i.material-icons arrow_right + span= tocSubItem.title + if tocIdx < page.toc.length - 1 + .toc-divider.inset + if injectCode.body + != injectCode.body diff --git a/server/views/master.pug b/server/views/master.pug index 6e582a9f..4a7f4da1 100644 --- a/server/views/master.pug +++ b/server/views/master.pug @@ -31,6 +31,26 @@ html script. var siteConfig = !{JSON.stringify(siteConfig)}; var siteLangs = !{JSON.stringify(langs)} + //- Icon Set + if config.theming.iconset === 'mdi' + link( + type='text/css' + rel='stylesheet' + href='https://cdn.materialdesignicons.com/3.7.95/css/materialdesignicons.min.css' + ) + else if config.theming.iconset === 'fa' + link( + type='text/css' + rel='stylesheet' + href='https://use.fontawesome.com/releases/v5.0.13/css/all.css' + ) + else if config.theming.iconset === 'fa4' + link( + type='text/css' + rel='stylesheet' + href='https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css' + ) + //- CSS diff --git a/yarn.lock b/yarn.lock index b778b1d6..94da4136 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3340,6 +3340,14 @@ browserslist@^4.6.1: electron-to-chromium "^1.3.164" node-releases "^1.1.23" +brute-knex@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/brute-knex/-/brute-knex-4.0.0.tgz#bb23549017565983e5ed7858214d8d15b690c3bb" + integrity sha512-mMpMvCJjWasupvbcYVPIb6QSWT67U8zKzp+nG6NRsQUeXJHE2fS76EG5r+NSzoQO7xKZ7kONnNkylnivZ9ASmA== + dependencies: + express-brute "^1.0.1" + knex "^0.17" + bser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -5720,7 +5728,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -express-brute@1.0.1: +express-brute@1.0.1, express-brute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/express-brute/-/express-brute-1.0.1.tgz#9f36d107fe34e40a682593e39bffcc53102b5335" integrity sha1-nzbRB/405ApoJZPjm//MUxArUzU= @@ -8141,7 +8149,7 @@ klaw@3.0.0: dependencies: graceful-fs "^4.1.9" -knex@0.17.6: +knex@0.17.6, knex@^0.17: version "0.17.6" resolved "https://registry.yarnpkg.com/knex/-/knex-0.17.6.tgz#80220cf159cd52768d5b29118c70b18aaf5138fe" integrity sha512-4SKp8jaBxqlEoaveenmpfnHEv5Kzo6/vhIj8UhW1srGw/FKqARTr+7Fv8C1C1qeVHDjv0coQWuUzN5eermHUsw==