more comments

This commit is contained in:
Evert Prants 2017-08-27 14:48:47 +03:00
parent e92bcf5521
commit c6824bff01
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
10 changed files with 99 additions and 35 deletions

View File

@ -12,21 +12,19 @@ Icy Network is a community network aimed at anyone who likes friendly discussion
More to come!
## 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
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
Clone this repository and then
1. `npm install` to get all the packages
2. `cp config.example.toml config.toml` copy the configuration
3. `npm run watch` to run the style and front-end script building watch task
4. `npm start -- -d` to start the application in development mode
1. Clone this repository and `cd` into it
2. `npm install` - Get all the dependencies
3. `cp config.example.toml config.toml` - Copy the configuration
4. `npm run watch` - Run the style and front-end script watch task
5. `npm start -- -d` - 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.
### Production
1. `npm run build`
2. `npm start`
1. `npm run build` - Build the front-end
2. `npm start` - Start the application in production mode

View File

@ -4,11 +4,12 @@
port=8282
# Session key
session_key="Session"
# Session secret
# Session secret (keep this a secret)
session_secret="hackmysessions"
# Number of worker processes (0 to use all CPU cores)
workers=1
# Domain of this application
# Used for the links in emails
domain="http://localhost:8282"
# Database

11
package-lock.json generated
View File

@ -2090,7 +2090,7 @@
"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",
"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",
"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",
@ -2131,7 +2131,7 @@
"debug": "https://registry.npmjs.org/debug/-/debug-2.6.7.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",
"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",
"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",
"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",
"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",
"unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
},
@ -3835,7 +3835,8 @@
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY="
},
"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="
},
"path-browserify": {
@ -4639,7 +4640,7 @@
"requires": {
"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",
"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"
}
},

View File

@ -1,6 +1,5 @@
import express from 'express'
import RateLimit from 'express-rate-limit'
import path from 'path'
import multiparty from 'multiparty'
import config from '../../scripts/load-config'
import wrap from '../../scripts/asyncRoute'
@ -9,16 +8,17 @@ import News from '../api/news'
import Image from '../api/image'
import APIExtern from '../api/external'
// const userContent = path.join(__dirname, '../..', 'usercontent')
let router = express.Router()
let dev = process.env.NODE_ENV !== 'production'
// Restrict API usage
let apiLimiter = new RateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 100,
delayMs: 0
})
// Restrict image uploads
let uploadLimiter = new RateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
@ -81,7 +81,8 @@ function JsonData (req, res, error, redirect = '/') {
* Ajax POST only <in-page javascript handeled>
* 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)
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) => {
if (!req.params.page || isNaN(parseInt(req.params.page))) {
return res.status(400).jsonp({error: 'Invalid page number.'})
}
let page = parseInt(req.params.page)
let articles = await News.listNews(page)
res.jsonp(articles)
@ -277,8 +283,8 @@ router.get('/news/:id', wrap(async (req, res) => {
}
let id = parseInt(req.params.id)
let article = await News.article(id)
res.jsonp(article)
}))
@ -370,6 +376,7 @@ router.use('/avatar', (req, res) => {
* =====================
*/
// List authorizations
router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => {
if (!req.session.user) return next()
@ -379,7 +386,8 @@ router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => {
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()
let clientId = parseInt(req.body.client_id)

View File

@ -2,7 +2,6 @@ import fs from 'fs'
import path from 'path'
import express from 'express'
import RateLimit from 'express-rate-limit'
import parseurl from 'parseurl'
import config from '../../scripts/load-config'
import wrap from '../../scripts/asyncRoute'
import http from '../../scripts/http'
@ -15,6 +14,7 @@ import oauthRouter from './oauth2'
let router = express.Router()
// Restrict account creation
let accountLimiter = new RateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
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.'
})
// Set the user session
function setSession (req, user) {
req.session.user = {
id: user.id,
@ -34,6 +35,7 @@ function setSession (req, user) {
}
router.use(wrap(async (req, res, next) => {
// Add form messages into the template rendering if present
let messages = req.flash('message')
if (!messages || !messages.length) {
messages = {}
@ -85,6 +87,8 @@ function extraButtons (req, res, 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) {
if (req.session.user) return next()
req.session.redirectUri = req.originalUrl
@ -124,6 +128,7 @@ router.get('/register', extraButtons, (req, res) => {
res.render('user/register')
})
// View for enabling Two-Factor Authentication
router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => {
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
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 })
}))
// View for disabling Two-Factor Authentication
router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => {
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')
}))
// Two-Factor Authentication verification on login
router.get('/login/verify', (req, res) => {
res.render('user/totp-check')
})
// User settings page
router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
let totpEnabled = false
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})
}))
// Change password
router.get('/user/manage/password', ensureLogin, wrap(async (req, res) => {
res.render('user/password_new')
}))
// Change email
router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => {
let obfuscated = req.session.user.email
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) {
// Security measures: never store any passwords in any session
for (let key in req.body) {
@ -213,9 +224,10 @@ function formError (req, res, error, redirect) {
req.flash('formkeep', req.body || {})
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) {
let output = ''
for (let i = 0; i < input.length; i++) {
@ -224,6 +236,7 @@ function cleanString (input) {
return output
}
// Enabling 2fa
router.post('/user/two-factor', wrap(async (req, res, next) => {
if (!req.session.user) return next()
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)
if (!verified) {
return formError(req, res, 'Try again!')
return formError(req, res, 'Something went wrong! Try scanning the code again.')
}
res.redirect('/')
}))
// Disabling 2fa
router.post('/user/two-factor/disable', wrap(async (req, res, next) => {
if (!req.session.user) return next()
if (req.body.csrf !== req.session.csrf) {
@ -260,6 +274,7 @@ router.post('/user/two-factor/disable', wrap(async (req, res, next) => {
res.redirect('/')
}))
// Verify 2FA for login
router.post('/login/verify', wrap(async (req, res, next) => {
if (req.session.user) return next()
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)
}))
// Log the user in
router.post('/login', wrap(async (req, res, next) => {
if (req.session.user) return next()
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.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)
if (totpRequired) {
req.session.totp_check = user.id
@ -347,6 +364,7 @@ router.post('/login', wrap(async (req, res, next) => {
res.redirect(uri)
}))
// Protected & Limited resource: Account registration
router.post('/register', accountLimiter, wrap(async (req, res, next) => {
if (req.session.user) return next()
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)
}
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')
}))
// Change display name
router.post('/user/manage', wrap(async (req, res, 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.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')
}))
// Change user password
router.post('/user/manage/password', wrap(async (req, res, 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')
}))
// Change email address
router.post('/user/manage/email', wrap(async (req, res, 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')
router.get('/docs/:name', (req, res, next) => {
let doc = path.join(docsDir, req.params.name + '.html')
@ -591,9 +619,11 @@ router.get('/docs/:name', (req, res, next) => {
return next(e)
}
res.header('Cache-Control', 'max-age=' + 7 * 24 * 60 * 60 * 1000) // 1 week
res.render('document', {doc: doc})
})
// Serve news
router.get('/news/:id?-*', wrap(async (req, res) => {
let id = parseInt(req.params.id)
if (isNaN(id)) {
@ -605,6 +635,7 @@ router.get('/news/:id?-*', wrap(async (req, res) => {
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})
}))
@ -619,6 +650,7 @@ router.get('/news/', wrap(async (req, res) => {
res.render('news', {news: news})
}))
// Render partials
router.get('/partials/:view', wrap(async (req, res, next) => {
if (!req.params.view) return next()
@ -639,6 +671,7 @@ router.get('/logout', wrap(async (req, res) => {
res.redirect('/')
}))
// User activation endpoint (emailed link)
router.get('/activate/:token', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login')
let token = req.params.token

View File

@ -11,7 +11,7 @@ router.use(oauth.express())
let oauthLimiter = new RateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 100,
max: 10,
delayMs: 0
})
@ -23,10 +23,12 @@ function ensureLoggedIn (req, res, next) {
res.redirect('/login')
}
// Generic OAuth2 endpoints
router.use('/authorize', ensureLoggedIn, oauth.controller.authorization)
router.post('/token', oauth.controller.token)
router.post('/introspect', oauth.controller.introspection)
// Protected user information resource
router.get('/user', oauth.bearer, wrap(async (req, res) => {
let accessToken = req.oauth2.accessToken
let user = await uapi.User.get(accessToken.user_id)

View File

@ -31,17 +31,21 @@ app.use(session({
}))
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
if (ipAddr.indexOf('::ffff:') !== -1) {
ipAddr = ipAddr.replace('::ffff:', '')
}
req.realIP = ipAddr
// Make sure CSRF token is present in the session
if (!req.session.csrf) {
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, {
user: req.session.user || null,
csrf: req.session.csrf
@ -56,8 +60,10 @@ module.exports = (args) => {
app.set('views', path.join(__dirname, '../views'))
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('/script', express.static(path.join(__dirname, '../build/script'), { maxAge: staticAge }))
app.use('/static', express.static(path.join(__dirname, '../static'), { maxAge: staticAge }))
@ -67,6 +73,8 @@ module.exports = (args) => {
app.listen(args.port, () => {
console.log('Listening on 0.0.0.0:' + args.port)
// Initialize the email transporter (if configured)
email.init()
})
}

View File

@ -39,7 +39,7 @@ $(document).ready(function () {
function removeAuthorization (clientId) {
$.ajax({
type: 'post',
url: '/api/oauth2/authorized-clients/delete',
url: '/api/oauth2/authorized-clients/revoke',
data: { client_id: clientId },
success: function (data) {
loadAuthorizations()

View File

@ -458,6 +458,17 @@ span.load
.option
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
display: block
position: fixed

View File

@ -31,6 +31,7 @@ block body
input(type="submit", value="Save Settings")
.right
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
if twitter_auth == false
a.option.accdisconnect(href="/api/external/twitter/remove")
@ -61,7 +62,8 @@ block body
i.fa.fa-fw.fa-envelope
|Change Email Address
.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
span.load
i.fa.fa-spin.fa-spinner.fa-2x