more comments
This commit is contained in:
parent
e92bcf5521
commit
c6824bff01
20
README.md
20
README.md
@ -12,21 +12,19 @@ Icy Network is a community network aimed at anyone who likes friendly discussion
|
|||||||
More to come!
|
More to come!
|
||||||
|
|
||||||
## The Goal of this Application
|
## The Goal of this Application
|
||||||
This application is used for authentication services such as OAuth2 in order to unite our websites with a single login and as a centeral news outlet for Icy Network services.
|
This application is used for authentication services such as OAuth2 in order to unite our websites with a single login and as a central news outlet for Icy Network services.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
The first time you run the application, it will migrate the database and that may take a while.
|
The first time you run the application, it will migrate the database and that may take a while. **You will also need a running instance of `redis-server` for session storage!**
|
||||||
### Development
|
### Development
|
||||||
Clone this repository and then
|
1. Clone this repository and `cd` into it
|
||||||
|
2. `npm install` - Get all the dependencies
|
||||||
1. `npm install` to get all the packages
|
3. `cp config.example.toml config.toml` - Copy the configuration
|
||||||
2. `cp config.example.toml config.toml` copy the configuration
|
4. `npm run watch` - Run the style and front-end script watch task
|
||||||
3. `npm run watch` to run the style and front-end script building watch task
|
5. `npm start -- -d` - Start the application in development mode
|
||||||
4. `npm start -- -d` to start the application in development mode
|
|
||||||
|
|
||||||
There is also a watch mode for the server. To enable `server` file tree watching you must provide both `-d` and `-w` as parameters. This task will reset all workers when any file in the `server` directory changes, enabling for live debugging.
|
There is also a watch mode for the server. To enable `server` file tree watching you must provide both `-d` and `-w` as parameters. This task will reset all workers when any file in the `server` directory changes, enabling for live debugging.
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
|
1. `npm run build` - Build the front-end
|
||||||
1. `npm run build`
|
2. `npm start` - Start the application in production mode
|
||||||
2. `npm start`
|
|
||||||
|
@ -4,11 +4,12 @@
|
|||||||
port=8282
|
port=8282
|
||||||
# Session key
|
# Session key
|
||||||
session_key="Session"
|
session_key="Session"
|
||||||
# Session secret
|
# Session secret (keep this a secret)
|
||||||
session_secret="hackmysessions"
|
session_secret="hackmysessions"
|
||||||
# Number of worker processes (0 to use all CPU cores)
|
# Number of worker processes (0 to use all CPU cores)
|
||||||
workers=1
|
workers=1
|
||||||
# Domain of this application
|
# Domain of this application
|
||||||
|
# Used for the links in emails
|
||||||
domain="http://localhost:8282"
|
domain="http://localhost:8282"
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
11
package-lock.json
generated
11
package-lock.json
generated
@ -2090,7 +2090,7 @@
|
|||||||
"merge-descriptors": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
"merge-descriptors": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||||
"methods": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
"methods": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
"on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
"on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
"parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
"parseurl": "1.3.1",
|
||||||
"path-to-regexp": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"path-to-regexp": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||||
"proxy-addr": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.4.tgz",
|
"proxy-addr": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.4.tgz",
|
||||||
"qs": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
|
"qs": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
|
||||||
@ -2131,7 +2131,7 @@
|
|||||||
"debug": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz",
|
"debug": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz",
|
||||||
"depd": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
|
"depd": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
|
||||||
"on-headers": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
|
"on-headers": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
|
||||||
"parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
"parseurl": "1.3.1",
|
||||||
"uid-safe": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.4.tgz",
|
"uid-safe": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.4.tgz",
|
||||||
"utils-merge": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
|
"utils-merge": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
|
||||||
},
|
},
|
||||||
@ -2222,7 +2222,7 @@
|
|||||||
"encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
|
"encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
|
||||||
"escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
"on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
"on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
"parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
"parseurl": "1.3.1",
|
||||||
"statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
|
"statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
|
||||||
"unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
"unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||||
},
|
},
|
||||||
@ -3835,7 +3835,8 @@
|
|||||||
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY="
|
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY="
|
||||||
},
|
},
|
||||||
"parseurl": {
|
"parseurl": {
|
||||||
"version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
||||||
"integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY="
|
"integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY="
|
||||||
},
|
},
|
||||||
"path-browserify": {
|
"path-browserify": {
|
||||||
@ -4639,7 +4640,7 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
|
"encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
|
||||||
"escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
"parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
"parseurl": "1.3.1",
|
||||||
"send": "https://registry.npmjs.org/send/-/send-0.15.3.tgz"
|
"send": "https://registry.npmjs.org/send/-/send-0.15.3.tgz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import RateLimit from 'express-rate-limit'
|
import RateLimit from 'express-rate-limit'
|
||||||
import path from 'path'
|
|
||||||
import multiparty from 'multiparty'
|
import multiparty from 'multiparty'
|
||||||
import config from '../../scripts/load-config'
|
import config from '../../scripts/load-config'
|
||||||
import wrap from '../../scripts/asyncRoute'
|
import wrap from '../../scripts/asyncRoute'
|
||||||
@ -9,16 +8,17 @@ import News from '../api/news'
|
|||||||
import Image from '../api/image'
|
import Image from '../api/image'
|
||||||
import APIExtern from '../api/external'
|
import APIExtern from '../api/external'
|
||||||
|
|
||||||
// const userContent = path.join(__dirname, '../..', 'usercontent')
|
|
||||||
|
|
||||||
let router = express.Router()
|
let router = express.Router()
|
||||||
|
let dev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
|
// Restrict API usage
|
||||||
let apiLimiter = new RateLimit({
|
let apiLimiter = new RateLimit({
|
||||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||||
max: 100,
|
max: 100,
|
||||||
delayMs: 0
|
delayMs: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Restrict image uploads
|
||||||
let uploadLimiter = new RateLimit({
|
let uploadLimiter = new RateLimit({
|
||||||
windowMs: 60 * 60 * 1000, // 1 hour
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
max: 10,
|
max: 10,
|
||||||
@ -81,7 +81,8 @@ function JsonData (req, res, error, redirect = '/') {
|
|||||||
* Ajax POST only <in-page javascript handeled>
|
* Ajax POST only <in-page javascript handeled>
|
||||||
* No tokens saved in configs, everything works out-of-the-box
|
* No tokens saved in configs, everything works out-of-the-box
|
||||||
*/
|
*/
|
||||||
router.post('/external/facebook/callback', wrap(async (req, res) => {
|
router.post('/external/facebook/callback', wrap(async (req, res, next) => {
|
||||||
|
if (!config.facebook || !config.facebook.client) return next()
|
||||||
let sane = objectAssembler(req.body)
|
let sane = objectAssembler(req.body)
|
||||||
sane.ip_address = req.realIP
|
sane.ip_address = req.realIP
|
||||||
|
|
||||||
@ -252,14 +253,19 @@ router.get('/external/discord/remove', wrap(async (req, res) => {
|
|||||||
* ========
|
* ========
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Get page of articles
|
// Cache news for one day
|
||||||
|
router.get('/news', (req, res, next) => {
|
||||||
|
if (!dev) res.header('Cache-Control', 'max-age=' + 24 * 60 * 60 * 1000) // 1 day
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get a page of articles
|
||||||
router.get('/news/all/:page', wrap(async (req, res) => {
|
router.get('/news/all/:page', wrap(async (req, res) => {
|
||||||
if (!req.params.page || isNaN(parseInt(req.params.page))) {
|
if (!req.params.page || isNaN(parseInt(req.params.page))) {
|
||||||
return res.status(400).jsonp({error: 'Invalid page number.'})
|
return res.status(400).jsonp({error: 'Invalid page number.'})
|
||||||
}
|
}
|
||||||
|
|
||||||
let page = parseInt(req.params.page)
|
let page = parseInt(req.params.page)
|
||||||
|
|
||||||
let articles = await News.listNews(page)
|
let articles = await News.listNews(page)
|
||||||
|
|
||||||
res.jsonp(articles)
|
res.jsonp(articles)
|
||||||
@ -277,8 +283,8 @@ router.get('/news/:id', wrap(async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let id = parseInt(req.params.id)
|
let id = parseInt(req.params.id)
|
||||||
|
|
||||||
let article = await News.article(id)
|
let article = await News.article(id)
|
||||||
|
|
||||||
res.jsonp(article)
|
res.jsonp(article)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -370,6 +376,7 @@ router.use('/avatar', (req, res) => {
|
|||||||
* =====================
|
* =====================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// List authorizations
|
||||||
router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => {
|
router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => {
|
||||||
if (!req.session.user) return next()
|
if (!req.session.user) return next()
|
||||||
|
|
||||||
@ -379,7 +386,8 @@ router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => {
|
|||||||
res.jsonp(list)
|
res.jsonp(list)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
router.post('/oauth2/authorized-clients/delete', wrap(async (req, res, next) => {
|
// Revoke an authorization
|
||||||
|
router.post('/oauth2/authorized-clients/revoke', wrap(async (req, res, next) => {
|
||||||
if (!req.session.user) return next()
|
if (!req.session.user) return next()
|
||||||
|
|
||||||
let clientId = parseInt(req.body.client_id)
|
let clientId = parseInt(req.body.client_id)
|
||||||
|
@ -2,7 +2,6 @@ import fs from 'fs'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import RateLimit from 'express-rate-limit'
|
import RateLimit from 'express-rate-limit'
|
||||||
import parseurl from 'parseurl'
|
|
||||||
import config from '../../scripts/load-config'
|
import config from '../../scripts/load-config'
|
||||||
import wrap from '../../scripts/asyncRoute'
|
import wrap from '../../scripts/asyncRoute'
|
||||||
import http from '../../scripts/http'
|
import http from '../../scripts/http'
|
||||||
@ -15,6 +14,7 @@ import oauthRouter from './oauth2'
|
|||||||
|
|
||||||
let router = express.Router()
|
let router = express.Router()
|
||||||
|
|
||||||
|
// Restrict account creation
|
||||||
let accountLimiter = new RateLimit({
|
let accountLimiter = new RateLimit({
|
||||||
windowMs: 60 * 60 * 1000, // 1 hour
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
max: 10,
|
max: 10,
|
||||||
@ -22,6 +22,7 @@ let accountLimiter = new RateLimit({
|
|||||||
message: 'Whoa, slow down there, buddy! You just hit our rate limits. Try again in 1 hour.'
|
message: 'Whoa, slow down there, buddy! You just hit our rate limits. Try again in 1 hour.'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set the user session
|
||||||
function setSession (req, user) {
|
function setSession (req, user) {
|
||||||
req.session.user = {
|
req.session.user = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@ -34,6 +35,7 @@ function setSession (req, user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
router.use(wrap(async (req, res, next) => {
|
router.use(wrap(async (req, res, next) => {
|
||||||
|
// Add form messages into the template rendering if present
|
||||||
let messages = req.flash('message')
|
let messages = req.flash('message')
|
||||||
if (!messages || !messages.length) {
|
if (!messages || !messages.length) {
|
||||||
messages = {}
|
messages = {}
|
||||||
@ -85,6 +87,8 @@ function extraButtons (req, res, next) {
|
|||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure the user is logged in
|
||||||
|
// Redirect to login page and store the current path in the session for redirecting later
|
||||||
function ensureLogin (req, res, next) {
|
function ensureLogin (req, res, next) {
|
||||||
if (req.session.user) return next()
|
if (req.session.user) return next()
|
||||||
req.session.redirectUri = req.originalUrl
|
req.session.redirectUri = req.originalUrl
|
||||||
@ -124,6 +128,7 @@ router.get('/register', extraButtons, (req, res) => {
|
|||||||
res.render('user/register')
|
res.render('user/register')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// View for enabling Two-Factor Authentication
|
||||||
router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => {
|
router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => {
|
||||||
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
|
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
|
||||||
if (twoFaEnabled) return res.redirect('/')
|
if (twoFaEnabled) return res.redirect('/')
|
||||||
@ -134,6 +139,7 @@ router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => {
|
|||||||
res.render('user/totp', { uri: newToken })
|
res.render('user/totp', { uri: newToken })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// View for disabling Two-Factor Authentication
|
||||||
router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => {
|
router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => {
|
||||||
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
|
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
|
||||||
|
|
||||||
@ -141,10 +147,12 @@ router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => {
|
|||||||
res.render('user/password')
|
res.render('user/password')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Two-Factor Authentication verification on login
|
||||||
router.get('/login/verify', (req, res) => {
|
router.get('/login/verify', (req, res) => {
|
||||||
res.render('user/totp-check')
|
res.render('user/totp-check')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// User settings page
|
||||||
router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
|
router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
|
||||||
let totpEnabled = false
|
let totpEnabled = false
|
||||||
let socialStatus = await API.User.socialStatus(req.session.user)
|
let socialStatus = await API.User.socialStatus(req.session.user)
|
||||||
@ -180,10 +188,12 @@ router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
|
|||||||
res.render('user/settings', {totp: totpEnabled, password: socialStatus.password})
|
res.render('user/settings', {totp: totpEnabled, password: socialStatus.password})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Change password
|
||||||
router.get('/user/manage/password', ensureLogin, wrap(async (req, res) => {
|
router.get('/user/manage/password', ensureLogin, wrap(async (req, res) => {
|
||||||
res.render('user/password_new')
|
res.render('user/password_new')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Change email
|
||||||
router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => {
|
router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => {
|
||||||
let obfuscated = req.session.user.email
|
let obfuscated = req.session.user.email
|
||||||
if (obfuscated) {
|
if (obfuscated) {
|
||||||
@ -203,6 +213,7 @@ router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => {
|
|||||||
=================
|
=================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Used to display errors on forms and save data
|
||||||
function formError (req, res, error, redirect) {
|
function formError (req, res, error, redirect) {
|
||||||
// Security measures: never store any passwords in any session
|
// Security measures: never store any passwords in any session
|
||||||
for (let key in req.body) {
|
for (let key in req.body) {
|
||||||
@ -213,9 +224,10 @@ function formError (req, res, error, redirect) {
|
|||||||
|
|
||||||
req.flash('formkeep', req.body || {})
|
req.flash('formkeep', req.body || {})
|
||||||
req.flash('message', {error: true, text: error})
|
req.flash('message', {error: true, text: error})
|
||||||
res.redirect(redirect || parseurl(req).path)
|
res.redirect(redirect || req.originalUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure characters are UTF-8
|
||||||
function cleanString (input) {
|
function cleanString (input) {
|
||||||
let output = ''
|
let output = ''
|
||||||
for (let i = 0; i < input.length; i++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
@ -224,6 +236,7 @@ function cleanString (input) {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enabling 2fa
|
||||||
router.post('/user/two-factor', wrap(async (req, res, next) => {
|
router.post('/user/two-factor', wrap(async (req, res, next) => {
|
||||||
if (!req.session.user) return next()
|
if (!req.session.user) return next()
|
||||||
if (!req.body.code) {
|
if (!req.body.code) {
|
||||||
@ -236,12 +249,13 @@ router.post('/user/two-factor', wrap(async (req, res, next) => {
|
|||||||
|
|
||||||
let verified = await API.User.Login.totpCheck(req.session.user, req.body.code)
|
let verified = await API.User.Login.totpCheck(req.session.user, req.body.code)
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
return formError(req, res, 'Try again!')
|
return formError(req, res, 'Something went wrong! Try scanning the code again.')
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect('/')
|
res.redirect('/')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Disabling 2fa
|
||||||
router.post('/user/two-factor/disable', wrap(async (req, res, next) => {
|
router.post('/user/two-factor/disable', wrap(async (req, res, next) => {
|
||||||
if (!req.session.user) return next()
|
if (!req.session.user) return next()
|
||||||
if (req.body.csrf !== req.session.csrf) {
|
if (req.body.csrf !== req.session.csrf) {
|
||||||
@ -260,6 +274,7 @@ router.post('/user/two-factor/disable', wrap(async (req, res, next) => {
|
|||||||
res.redirect('/')
|
res.redirect('/')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Verify 2FA for login
|
||||||
router.post('/login/verify', wrap(async (req, res, next) => {
|
router.post('/login/verify', wrap(async (req, res, next) => {
|
||||||
if (req.session.user) return next()
|
if (req.session.user) return next()
|
||||||
if (req.session.totp_check === null) return res.redirect('/login')
|
if (req.session.totp_check === null) return res.redirect('/login')
|
||||||
@ -302,6 +317,7 @@ router.post('/login/verify', wrap(async (req, res, next) => {
|
|||||||
res.redirect(uri)
|
res.redirect(uri)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Log the user in
|
||||||
router.post('/login', wrap(async (req, res, next) => {
|
router.post('/login', wrap(async (req, res, next) => {
|
||||||
if (req.session.user) return next()
|
if (req.session.user) return next()
|
||||||
if (!req.body.username || !req.body.password || req.body.username === '') {
|
if (!req.body.username || !req.body.password || req.body.username === '') {
|
||||||
@ -323,6 +339,7 @@ router.post('/login', wrap(async (req, res, next) => {
|
|||||||
if (user.activated === 0) return formError(req, res, 'Please activate your account first.')
|
if (user.activated === 0) return formError(req, res, 'Please activate your account first.')
|
||||||
if (user.locked === 1) return formError(req, res, 'This account has been locked.')
|
if (user.locked === 1) return formError(req, res, 'This account has been locked.')
|
||||||
|
|
||||||
|
// Redirect to the verification dialog if 2FA is enabled
|
||||||
let totpRequired = await API.User.Login.totpTokenRequired(user)
|
let totpRequired = await API.User.Login.totpTokenRequired(user)
|
||||||
if (totpRequired) {
|
if (totpRequired) {
|
||||||
req.session.totp_check = user.id
|
req.session.totp_check = user.id
|
||||||
@ -347,6 +364,7 @@ router.post('/login', wrap(async (req, res, next) => {
|
|||||||
res.redirect(uri)
|
res.redirect(uri)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Protected & Limited resource: Account registration
|
||||||
router.post('/register', accountLimiter, wrap(async (req, res, next) => {
|
router.post('/register', accountLimiter, wrap(async (req, res, next) => {
|
||||||
if (req.session.user) return next()
|
if (req.session.user) return next()
|
||||||
if (!req.body.username || !req.body.display_name || !req.body.password || !req.body.email) {
|
if (!req.body.username || !req.body.display_name || !req.body.password || !req.body.email) {
|
||||||
@ -423,10 +441,17 @@ router.post('/register', accountLimiter, wrap(async (req, res, next) => {
|
|||||||
return formError(req, res, newUser.error)
|
return formError(req, res, newUser.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.flash('message', {error: false, text: 'Account created successfully! Please check your email for an activation link.'})
|
// Do not include activation link message when the user is already activated
|
||||||
|
let registerMessage = 'Account created successfully!'
|
||||||
|
if (newUser.user && newUser.user.activated !== 1) {
|
||||||
|
registerMessage += ' Please check your email for an activation link.'
|
||||||
|
}
|
||||||
|
|
||||||
|
req.flash('message', {error: false, text: registerMessage})
|
||||||
res.redirect('/login')
|
res.redirect('/login')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Change display name
|
||||||
router.post('/user/manage', wrap(async (req, res, next) => {
|
router.post('/user/manage', wrap(async (req, res, next) => {
|
||||||
if (!req.session.user) return next()
|
if (!req.session.user) return next()
|
||||||
|
|
||||||
@ -460,10 +485,11 @@ router.post('/user/manage', wrap(async (req, res, next) => {
|
|||||||
|
|
||||||
req.session.user.display_name = displayName
|
req.session.user.display_name = displayName
|
||||||
|
|
||||||
req.flash('message', {error: false, text: 'Settings changed successfully. Please note that it may take time to update on other websites and devices.'})
|
req.flash('message', {error: false, text: 'Settings changed successfully.'})
|
||||||
res.redirect('/user/manage')
|
res.redirect('/user/manage')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Change user password
|
||||||
router.post('/user/manage/password', wrap(async (req, res, next) => {
|
router.post('/user/manage/password', wrap(async (req, res, next) => {
|
||||||
if (!req.session.user) return next()
|
if (!req.session.user) return next()
|
||||||
|
|
||||||
@ -514,6 +540,7 @@ router.post('/user/manage/password', wrap(async (req, res, next) => {
|
|||||||
return res.redirect('/user/manage')
|
return res.redirect('/user/manage')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Change email address
|
||||||
router.post('/user/manage/email', wrap(async (req, res, next) => {
|
router.post('/user/manage/email', wrap(async (req, res, next) => {
|
||||||
if (!req.session.user) return next()
|
if (!req.session.user) return next()
|
||||||
|
|
||||||
@ -578,6 +605,7 @@ router.post('/user/manage/email', wrap(async (req, res, next) => {
|
|||||||
=============
|
=============
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Serve a document form the documents directory, cache it.
|
||||||
const docsDir = path.join(__dirname, '../../documents')
|
const docsDir = path.join(__dirname, '../../documents')
|
||||||
router.get('/docs/:name', (req, res, next) => {
|
router.get('/docs/:name', (req, res, next) => {
|
||||||
let doc = path.join(docsDir, req.params.name + '.html')
|
let doc = path.join(docsDir, req.params.name + '.html')
|
||||||
@ -591,9 +619,11 @@ router.get('/docs/:name', (req, res, next) => {
|
|||||||
return next(e)
|
return next(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.header('Cache-Control', 'max-age=' + 7 * 24 * 60 * 60 * 1000) // 1 week
|
||||||
res.render('document', {doc: doc})
|
res.render('document', {doc: doc})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Serve news
|
||||||
router.get('/news/:id?-*', wrap(async (req, res) => {
|
router.get('/news/:id?-*', wrap(async (req, res) => {
|
||||||
let id = parseInt(req.params.id)
|
let id = parseInt(req.params.id)
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
@ -605,6 +635,7 @@ router.get('/news/:id?-*', wrap(async (req, res) => {
|
|||||||
return res.status(404).render('article', {article: null})
|
return res.status(404).render('article', {article: null})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.header('Cache-Control', 'max-age=' + 24 * 60 * 60 * 1000) // 1 day
|
||||||
res.render('article', {article: article})
|
res.render('article', {article: article})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -619,6 +650,7 @@ router.get('/news/', wrap(async (req, res) => {
|
|||||||
res.render('news', {news: news})
|
res.render('news', {news: news})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Render partials
|
||||||
router.get('/partials/:view', wrap(async (req, res, next) => {
|
router.get('/partials/:view', wrap(async (req, res, next) => {
|
||||||
if (!req.params.view) return next()
|
if (!req.params.view) return next()
|
||||||
|
|
||||||
@ -639,6 +671,7 @@ router.get('/logout', wrap(async (req, res) => {
|
|||||||
res.redirect('/')
|
res.redirect('/')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// User activation endpoint (emailed link)
|
||||||
router.get('/activate/:token', wrap(async (req, res) => {
|
router.get('/activate/:token', wrap(async (req, res) => {
|
||||||
if (req.session.user) return res.redirect('/login')
|
if (req.session.user) return res.redirect('/login')
|
||||||
let token = req.params.token
|
let token = req.params.token
|
||||||
|
@ -11,7 +11,7 @@ router.use(oauth.express())
|
|||||||
|
|
||||||
let oauthLimiter = new RateLimit({
|
let oauthLimiter = new RateLimit({
|
||||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||||
max: 100,
|
max: 10,
|
||||||
delayMs: 0
|
delayMs: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -23,10 +23,12 @@ function ensureLoggedIn (req, res, next) {
|
|||||||
res.redirect('/login')
|
res.redirect('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic OAuth2 endpoints
|
||||||
router.use('/authorize', ensureLoggedIn, oauth.controller.authorization)
|
router.use('/authorize', ensureLoggedIn, oauth.controller.authorization)
|
||||||
router.post('/token', oauth.controller.token)
|
router.post('/token', oauth.controller.token)
|
||||||
router.post('/introspect', oauth.controller.introspection)
|
router.post('/introspect', oauth.controller.introspection)
|
||||||
|
|
||||||
|
// Protected user information resource
|
||||||
router.get('/user', oauth.bearer, wrap(async (req, res) => {
|
router.get('/user', oauth.bearer, wrap(async (req, res) => {
|
||||||
let accessToken = req.oauth2.accessToken
|
let accessToken = req.oauth2.accessToken
|
||||||
let user = await uapi.User.get(accessToken.user_id)
|
let user = await uapi.User.get(accessToken.user_id)
|
||||||
|
@ -31,17 +31,21 @@ app.use(session({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
|
// Inject a cleaner version of the user's IP Address into the request
|
||||||
let ipAddr = req.headers['x-forwarded-for'] || req.connection.remoteAddress
|
let ipAddr = req.headers['x-forwarded-for'] || req.connection.remoteAddress
|
||||||
|
|
||||||
if (ipAddr.indexOf('::ffff:') !== -1) {
|
if (ipAddr.indexOf('::ffff:') !== -1) {
|
||||||
ipAddr = ipAddr.replace('::ffff:', '')
|
ipAddr = ipAddr.replace('::ffff:', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req.realIP = ipAddr
|
||||||
|
|
||||||
|
// Make sure CSRF token is present in the session
|
||||||
if (!req.session.csrf) {
|
if (!req.session.csrf) {
|
||||||
req.session.csrf = crypto.randomBytes(12).toString('hex')
|
req.session.csrf = crypto.randomBytes(12).toString('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
req.realIP = ipAddr
|
// Add user and csrf token into rendering information
|
||||||
res.locals = Object.assign(res.locals, {
|
res.locals = Object.assign(res.locals, {
|
||||||
user: req.session.user || null,
|
user: req.session.user || null,
|
||||||
csrf: req.session.csrf
|
csrf: req.session.csrf
|
||||||
@ -56,8 +60,10 @@ module.exports = (args) => {
|
|||||||
app.set('views', path.join(__dirname, '../views'))
|
app.set('views', path.join(__dirname, '../views'))
|
||||||
|
|
||||||
if (args.dev) console.log('Worker is in development mode')
|
if (args.dev) console.log('Worker is in development mode')
|
||||||
let staticAge = args.dev ? 1000 : 7 * 24 * 60 * 60 * 1000
|
let staticAge = args.dev ? 1000 : 7 * 24 * 60 * 60 * 1000 // 1 week of cache in production
|
||||||
|
|
||||||
|
// Static content directories, cache these requests.
|
||||||
|
// It is also a good idea to use nginx to serve these directories in order to save on computing power
|
||||||
app.use('/style', express.static(path.join(__dirname, '../build/style'), { maxAge: staticAge }))
|
app.use('/style', express.static(path.join(__dirname, '../build/style'), { maxAge: staticAge }))
|
||||||
app.use('/script', express.static(path.join(__dirname, '../build/script'), { maxAge: staticAge }))
|
app.use('/script', express.static(path.join(__dirname, '../build/script'), { maxAge: staticAge }))
|
||||||
app.use('/static', express.static(path.join(__dirname, '../static'), { maxAge: staticAge }))
|
app.use('/static', express.static(path.join(__dirname, '../static'), { maxAge: staticAge }))
|
||||||
@ -67,6 +73,8 @@ module.exports = (args) => {
|
|||||||
|
|
||||||
app.listen(args.port, () => {
|
app.listen(args.port, () => {
|
||||||
console.log('Listening on 0.0.0.0:' + args.port)
|
console.log('Listening on 0.0.0.0:' + args.port)
|
||||||
|
|
||||||
|
// Initialize the email transporter (if configured)
|
||||||
email.init()
|
email.init()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ $(document).ready(function () {
|
|||||||
function removeAuthorization (clientId) {
|
function removeAuthorization (clientId) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: 'post',
|
type: 'post',
|
||||||
url: '/api/oauth2/authorized-clients/delete',
|
url: '/api/oauth2/authorized-clients/revoke',
|
||||||
data: { client_id: clientId },
|
data: { client_id: clientId },
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
loadAuthorizations()
|
loadAuthorizations()
|
||||||
|
@ -458,6 +458,17 @@ span.load
|
|||||||
.option
|
.option
|
||||||
display: block
|
display: block
|
||||||
|
|
||||||
|
.specify
|
||||||
|
display: inline-block
|
||||||
|
width: 22px
|
||||||
|
text-align: center
|
||||||
|
margin-left: 5px
|
||||||
|
background-color: #2196F3
|
||||||
|
color: #fff
|
||||||
|
border-radius: 100px
|
||||||
|
cursor: help
|
||||||
|
font-size: 19px
|
||||||
|
|
||||||
.dialog-drop
|
.dialog-drop
|
||||||
display: block
|
display: block
|
||||||
position: fixed
|
position: fixed
|
||||||
|
@ -31,6 +31,7 @@ block body
|
|||||||
input(type="submit", value="Save Settings")
|
input(type="submit", value="Save Settings")
|
||||||
.right
|
.right
|
||||||
h3 Social Media Accounts
|
h3 Social Media Accounts
|
||||||
|
.specify(title="You can add social media accounts to your account for ease of login. Once added, logging in from linked sources logs you into this account automatically.") ?
|
||||||
include ../includes/external.pug
|
include ../includes/external.pug
|
||||||
if twitter_auth == false
|
if twitter_auth == false
|
||||||
a.option.accdisconnect(href="/api/external/twitter/remove")
|
a.option.accdisconnect(href="/api/external/twitter/remove")
|
||||||
@ -61,7 +62,8 @@ block body
|
|||||||
i.fa.fa-fw.fa-envelope
|
i.fa.fa-fw.fa-envelope
|
||||||
|Change Email Address
|
|Change Email Address
|
||||||
.clients
|
.clients
|
||||||
h2 OAuth2 Authorized Applications
|
h2 Authorized Applications
|
||||||
|
.specify(title="Applications which have access to basic user information. You may restrict access at any time by pressing the red icon on the top right of the application card.") ?
|
||||||
.cl#clientlist
|
.cl#clientlist
|
||||||
span.load
|
span.load
|
||||||
i.fa.fa-spin.fa-spinner.fa-2x
|
i.fa.fa-spin.fa-spinner.fa-2x
|
||||||
|
Reference in New Issue
Block a user