Browse Source

feat: legacy login

NGPixel 5 năm trước cách đây
mục cha
commit
03e80bdff3

+ 115 - 1
client/scss/legacy.scss

@@ -28,6 +28,120 @@ body {
   min-height: 100vh;
   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 {
 .header {
   background-color: #000;
   background-color: #000;
   color: #FFF;
   color: #FFF;
@@ -145,7 +259,7 @@ body {
   }
   }
 
 
   &-right {
   &-right {
-    flex: 0 0 324px;
+    flex: 0 0 308px;
     padding-left: 16px;
     padding-left: 16px;
 
 
     &-title {
     &-title {

+ 2 - 51
dev/templates/legacy.pug

@@ -90,58 +90,9 @@ html
 
 
     != analyticsCode.head
     != analyticsCode.head
 
 
-    if injectCode.css
-      style(type='text/css')!= injectCode.css
-    if injectCode.head
-      != injectCode.head
+    block head
 
 
   body
   body
     != analyticsCode.bodyStart
     != 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
     != analyticsCode.bodyEnd

+ 1 - 1
dev/webpack/webpack.dev.js

@@ -199,7 +199,7 @@ module.exports = {
     }),
     }),
     new HtmlWebpackPlugin({
     new HtmlWebpackPlugin({
       template: 'dev/templates/legacy.pug',
       template: 'dev/templates/legacy.pug',
-      filename: '../server/views/legacy.pug',
+      filename: '../server/views/legacy/master.pug',
       hash: false,
       hash: false,
       inject: false,
       inject: false,
       excludeChunks: ['setup', 'app']
       excludeChunks: ['setup', 'app']

+ 1 - 1
dev/webpack/webpack.prod.js

@@ -209,7 +209,7 @@ module.exports = {
     }),
     }),
     new HtmlWebpackPlugin({
     new HtmlWebpackPlugin({
       template: 'dev/templates/legacy.pug',
       template: 'dev/templates/legacy.pug',
-      filename: '../server/views/legacy.pug',
+      filename: '../server/views/legacy/master.pug',
       hash: false,
       hash: false,
       inject: false,
       inject: false,
       excludeChunks: ['setup', 'app']
       excludeChunks: ['setup', 'app']

+ 1 - 0
package.json

@@ -47,6 +47,7 @@
     "bcryptjs-then": "1.0.1",
     "bcryptjs-then": "1.0.1",
     "bluebird": "3.5.5",
     "bluebird": "3.5.5",
     "body-parser": "1.19.0",
     "body-parser": "1.19.0",
+    "brute-knex": "4.0.0",
     "chalk": "2.4.2",
     "chalk": "2.4.2",
     "cheerio": "1.0.0-rc.3",
     "cheerio": "1.0.0-rc.3",
     "chokidar": "3.0.1",
     "chokidar": "3.0.1",

+ 85 - 3
server/controllers/auth.js

@@ -1,17 +1,70 @@
 /* global WIKI */
 /* global WIKI */
 
 
 const express = require('express')
 const express = require('express')
+const ExpressBrute = require('express-brute')
+const BruteKnex = require('brute-knex')
 const router = express.Router()
 const router = express.Router()
 const moment = require('moment')
 const moment = require('moment')
 const _ = require('lodash')
 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
  * Login form
  */
  */
-router.get('/login', (req, res, next) => {
+router.get('/login', async (req, res, next) => {
   _.set(res.locals, 'pageMeta.title', 'Login')
   _.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) => {
 router.get('/login/:strategy', async (req, res, next) => {
   try {
   try {
     await WIKI.models.users.login({
     await WIKI.models.users.login({
@@ -21,6 +74,10 @@ router.get('/login/:strategy', async (req, res, next) => {
     next(err)
     next(err)
   }
   }
 })
 })
+
+/**
+ * Social Strategies Callback
+ */
 router.all('/login/:strategy/callback', async (req, res, next) => {
 router.all('/login/:strategy/callback', async (req, res, next) => {
   if (req.method !== 'GET' && req.method !== 'POST') { return 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
  * Logout
  */
  */
@@ -59,10 +140,11 @@ router.get('/register', async (req, res, next) => {
 /**
 /**
  * Verify
  * 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 })
   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)
   await WIKI.models.users.query().patch({ isVerified: true }).where('id', usr.id)
   const result = await WIKI.models.users.refreshToken(usr)
   const result = await WIKI.models.users.refreshToken(usr)
+  req.brute.reset()
   res.cookie('jwt', result.token, { expires: moment().add(1, 'years').toDate() })
   res.cookie('jwt', result.token, { expires: moment().add(1, 'years').toDate() })
   res.redirect('/')
   res.redirect('/')
 })
 })

+ 1 - 1
server/controllers/common.js

@@ -200,7 +200,7 @@ router.get('/*', async (req, res, next) => {
           if (_.isString(page.toc)) {
           if (_.isString(page.toc)) {
             page.toc = JSON.parse(page.toc)
             page.toc = JSON.parse(page.toc)
           }
           }
-          res.render('legacy', { page, sidebar, injectCode })
+          res.render('legacy/page', { page, sidebar, injectCode })
         } else {
         } else {
           res.render('page', { page, sidebar, injectCode })
           res.render('page', { page, sidebar, injectCode })
         }
         }

+ 21 - 0
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

+ 82 - 0
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

+ 56 - 0
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

+ 20 - 0
server/views/master.pug

@@ -31,6 +31,26 @@ html
     script.
     script.
       var siteConfig = !{JSON.stringify(siteConfig)}; var siteLangs = !{JSON.stringify(langs)}
       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
     //- CSS
     
     
 
 

+ 10 - 2
yarn.lock

@@ -3340,6 +3340,14 @@ browserslist@^4.6.1:
     electron-to-chromium "^1.3.164"
     electron-to-chromium "^1.3.164"
     node-releases "^1.1.23"
     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:
 bser@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
   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:
   dependencies:
     homedir-polyfill "^1.0.1"
     homedir-polyfill "^1.0.1"
 
 
-express-brute@1.0.1:
+express-brute@1.0.1, express-brute@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/express-brute/-/express-brute-1.0.1.tgz#9f36d107fe34e40a682593e39bffcc53102b5335"
   resolved "https://registry.yarnpkg.com/express-brute/-/express-brute-1.0.1.tgz#9f36d107fe34e40a682593e39bffcc53102b5335"
   integrity sha1-nzbRB/405ApoJZPjm//MUxArUzU=
   integrity sha1-nzbRB/405ApoJZPjm//MUxArUzU=
@@ -8141,7 +8149,7 @@ klaw@3.0.0:
   dependencies:
   dependencies:
     graceful-fs "^4.1.9"
     graceful-fs "^4.1.9"
 
 
-knex@0.17.6:
+knex@0.17.6, knex@^0.17:
   version "0.17.6"
   version "0.17.6"
   resolved "https://registry.yarnpkg.com/knex/-/knex-0.17.6.tgz#80220cf159cd52768d5b29118c70b18aaf5138fe"
   resolved "https://registry.yarnpkg.com/knex/-/knex-0.17.6.tgz#80220cf159cd52768d5b29118c70b18aaf5138fe"
   integrity sha512-4SKp8jaBxqlEoaveenmpfnHEv5Kzo6/vhIj8UhW1srGw/FKqARTr+7Fv8C1C1qeVHDjv0coQWuUzN5eermHUsw==
   integrity sha512-4SKp8jaBxqlEoaveenmpfnHEv5Kzo6/vhIj8UhW1srGw/FKqARTr+7Fv8C1C1qeVHDjv0coQWuUzN5eermHUsw==