diff --git a/server/api/emailer.js b/server/api/emailer.js index 1779a1b..0135742 100644 --- a/server/api/emailer.js +++ b/server/api/emailer.js @@ -1,6 +1,7 @@ import {EmailTemplate} from 'email-templates' import path from 'path' import nodemailer from 'nodemailer' + import config from '../../scripts/load-config' const templateDir = path.join(__dirname, '../../', 'templates') diff --git a/server/api/external.js b/server/api/external.js index 306116b..74c44dd 100644 --- a/server/api/external.js +++ b/server/api/external.js @@ -1,12 +1,13 @@ +import qs from 'querystring' +import oauth from 'oauth-libre' +import uuidV1 from 'uuid/v1' +import crypto from 'crypto' + import config from '../../scripts/load-config' import http from '../../scripts/http' import models from './models' import Image from './image' import UAPI from './index' -import qs from 'querystring' -import oauth from 'oauth-libre' -import uuidV1 from 'uuid/v1' -import crypto from 'crypto' const userFields = ['username', 'email', 'avatar_file', 'display_name', 'ip_address'] @@ -74,7 +75,7 @@ const API = { udataLimited.username = udataLimited.username.substring(0, 26) // Check if the username is already taken - if (await UAPI.User.get(udataLimited.username) != null) { + if (await UAPI.User.get(udataLimited.username) != null || udataLimited.username.length < 4) { udataLimited.username = udataLimited.username + UAPI.Hash(4) } diff --git a/server/api/image.js b/server/api/image.js index ce89916..3e6fb11 100644 --- a/server/api/image.js +++ b/server/api/image.js @@ -1,8 +1,7 @@ import gm from 'gm' import url from 'url' import path from 'path' -import crypto from 'crypto' -import Promise from 'bluebird' +import uuid from 'uuid/v4' import http from '../../scripts/http' @@ -17,10 +16,6 @@ const imageTypes = { 'image/jpeg': '.jpeg' } -function imageUniquifier () { - return crypto.randomBytes(12).toString('hex') -} - function decodeBase64Image (dataString) { let matches = dataString.match(/^data:([A-Za-z-+/]+);base64,(.+)$/) let response = {} @@ -60,7 +55,7 @@ async function imageBase64 (baseObj) { if (!imgData) return null if (!imageTypes[imgData.type]) return null - let imageName = 'base64-' + imageUniquifier() + let imageName = 'base64-' + uuid() let ext = imageTypes[imgData.type] || '.png' imageName += ext @@ -81,7 +76,7 @@ async function downloadImage (imgUrl, designation) { if (!imgUrl) return null if (!designation) designation = 'download' - let imageName = designation + '-' + imageUniquifier() + let imageName = designation + '-' + uuid() let uridata = url.parse(imgUrl) let pathdata = path.parse(uridata.path) diff --git a/server/api/index.js b/server/api/index.js index f7d0f62..d7df691 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -1,15 +1,16 @@ import path from 'path' import cprog from 'child_process' -import config from '../../scripts/load-config' -import http from '../../scripts/http' -import models from './models' import crypto from 'crypto' import notp from 'notp' import base32 from 'thirty-two' -import emailer from './emailer' import uuidV1 from 'uuid/v1' import fs from 'fs-extra' +import config from '../../scripts/load-config' +import http from '../../scripts/http' +import models from './models' +import emailer from './emailer' + const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ // Fork a bcrypt process to hash and compare passwords @@ -533,7 +534,6 @@ const API = { console.debug('IPN Verified Notification:', body) } - // TODO: add database field for this if (body.txn_id) { if (txnStore.indexOf(body.txn_id) !== -1) return true txnStore.push(body.txn_id) diff --git a/server/api/minecraft.js b/server/api/minecraft.js index 4c72685..e7c6936 100644 --- a/server/api/minecraft.js +++ b/server/api/minecraft.js @@ -1,6 +1,7 @@ +import crypto from 'crypto' + import API from './index' import Model from './models' -import crypto from 'crypto' const mAPI = { getToken: async function (user) { diff --git a/server/index.js b/server/index.js index 060bdf1..1f72ef9 100644 --- a/server/index.js +++ b/server/index.js @@ -15,7 +15,7 @@ const args = { function spawnWorkers () { let workerCount = config.server.workers === 0 ? cpuCount : config.server.workers - console.log('Spinning up ' + workerCount + ' worker process' + (workerCount !== 1 ? 'es' : '')) + console.log('Spinning up %d worker process%s', workerCount, (workerCount !== 1 ? 'es' : '')) for (let i = 0; i < workerCount; i++) { spawnWorker() @@ -68,14 +68,14 @@ function spawnWorker (oldWorker) { w.process.stderr.on('data', (data) => { console.log(w.process.pid, data.toString().trim()) }) - args.verbose && console.log('Starting worker process ' + w.process.pid + '...') + args.verbose && console.log('Starting worker process %d...', w.process.pid) w.on('message', (message) => { if (message === 'started') { workers.push(w) - args.verbose && console.log('Started worker process ' + w.process.pid) + args.verbose && console.log('Started worker process', w.process.pid) if (oldWorker) { - args.verbose && console.log('Stopping worker process ' + oldWorker.process.pid) + args.verbose && console.log('Stopping worker process', oldWorker.process.pid) oldWorker.send('stop') } } else { @@ -99,7 +99,7 @@ cluster.setupMaster({ cluster.on('exit', (worker, code, signal) => { let extra = ((code || '') + ' ' + (signal || '')).trim() - console.error('Worker process ' + worker.process.pid + ' exited ' + (extra ? '(' + extra + ')' : '')) + console.error('Worker process %d exited %s', worker.process.pid, (extra ? '(' + extra + ')' : '')) let index = workers.indexOf(worker) diff --git a/server/routes/admin.js b/server/routes/admin.js index 2a898c4..4ab3a46 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -1,8 +1,9 @@ import express from 'express' + import ensureLogin from '../../scripts/ensureLogin' import wrap from '../../scripts/asyncRoute' -import {User} from '../api' import API from '../api/admin' +import {User} from '../api' const router = express.Router() const apiRouter = express.Router() diff --git a/server/routes/api.js b/server/routes/api.js index 2f0e207..70a0330 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -1,12 +1,13 @@ import express from 'express' import RateLimit from 'express-rate-limit' import multiparty from 'multiparty' + import config from '../../scripts/load-config' import wrap from '../../scripts/asyncRoute' -import API from '../api' -import News from '../api/news' -import Image from '../api/image' import APIExtern from '../api/external' +import Image from '../api/image' +import News from '../api/news' +import API from '../api' let router = express.Router() let dev = process.env.NODE_ENV !== 'production' diff --git a/server/routes/index.js b/server/routes/index.js index 48c8a21..93f1947 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -2,6 +2,7 @@ import fs from 'fs-extra' import path from 'path' import express from 'express' import RateLimit from 'express-rate-limit' + import ensureLogin from '../../scripts/ensureLogin' import config from '../../scripts/load-config' import wrap from '../../scripts/asyncRoute' @@ -78,7 +79,7 @@ router.use(wrap(async (req, res, next) => { } // Update user session - let udata = await API.User.get(req.session.user.id) + let udata = await API.User.get(req.session.user) setSession(req, udata) // Update IP address @@ -158,6 +159,7 @@ router.get('/reset/:token', wrap(async (req, res) => { router.get('/login', extraButtons, (req, res) => { if (req.session.user) return redirectLogin(req, res) + if (req.query.returnTo) { req.session.redirectUri = req.query.returnTo } @@ -175,6 +177,21 @@ router.get('/register', extraButtons, formKeep, (req, res) => { res.render('user/register') }) +// 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 + let success = await API.User.Login.activationToken(token) + + if (!success) { + req.flash('message', {error: true, text: 'Invalid or expired activation token.'}) + } else { + req.flash('message', {error: false, text: 'Your account has been activated! You may now log in.'}) + } + + res.redirect('/login') +})) + // 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) @@ -291,9 +308,11 @@ function formError (req, res, error, redirect) { // Make sure characters are UTF-8 function cleanString (input) { let output = '' + for (let i = 0; i < input.length; i++) { output += input.charCodeAt(i) <= 127 ? input.charAt(i) : '' } + return output } @@ -309,6 +328,7 @@ function csrfValidation (req, res, next) { // Enabling 2fa router.post('/user/two-factor', csrfValidation, wrap(async (req, res, next) => { if (!req.session.user) return next() + if (!req.body.code) { return formError(req, res, 'You need to enter the code.') } @@ -340,7 +360,9 @@ router.post('/user/two-factor/disable', csrfValidation, wrap(async (req, res, ne // Verify 2FA for login router.post('/login/verify', csrfValidation, wrap(async (req, res, next) => { if (req.session.user) return next() + if (req.session.totp_check === null) return res.redirect('/login') + if (!req.body.code && !req.body.recovery) { return formError(req, res, 'You need to enter the code.') } @@ -360,6 +382,7 @@ router.post('/login/verify', csrfValidation, wrap(async (req, res, next) => { // Log the user in. Limited resource router.post('/login', accountLimiter, csrfValidation, wrap(async (req, res, next) => { if (req.session.user) return next() + if (!req.body.username || !req.body.password || req.body.username === '') { return res.redirect('/login') } @@ -373,6 +396,7 @@ router.post('/login', accountLimiter, csrfValidation, wrap(async (req, res, next if (!pwMatch) return formError(req, res, 'Invalid username or password.') 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.') // Check if the user is banned @@ -407,6 +431,7 @@ router.post('/login', accountLimiter, csrfValidation, wrap(async (req, res, next // Password reset router.post('/login/reset', accountLimiter, csrfValidation, wrap(async (req, res, next) => { if (req.session.user) return next() + if (!req.body.email) { return formError(req, res, 'You need to enter your email address.') } @@ -420,7 +445,7 @@ router.post('/login/reset', accountLimiter, csrfValidation, wrap(async (req, res try { await API.User.Reset.reset(email) - req.flash('message', {error: false, text: 'We\'ve sent a link to your email address. Please check spam folders too!'}) + req.flash('message', {error: false, text: 'We\'ve sent a link to your email address. Please check spam folders, too!'}) res.redirect('/login/reset?success=true') } catch (e) { return formError(req, res, e.message) @@ -466,6 +491,7 @@ router.post('/reset/:token', csrfValidation, wrap(async (req, res) => { // Protected & Limited resource: Account registration router.post('/register', accountLimiter, csrfValidation, 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) { return formError(req, res, 'Please fill in all the fields.') } @@ -563,7 +589,7 @@ router.post('/user/manage', csrfValidation, wrap(async (req, res, next) => { let displayName = req.body.display_name if (!displayName || !displayName.match(/^([^\\`]{3,32})$/i)) { - return formError(req, res, 'Invalid display name!') + return formError(req, res, 'Invalid Display Name!') } displayName = cleanString(displayName) @@ -680,7 +706,6 @@ router.post('/user/manage/email', accountLimiter, csrfValidation, wrap(async (re return formError(req, res, e.message) } - // TODO: Send necessary emails console.warn('[SECURITY AUDIT] User \'%s\' email has been changed from %s', user.username, req.realIP) req.session.user.email = newEmail @@ -790,21 +815,6 @@ router.get('/logout', (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 - let success = await API.User.Login.activationToken(token) - - if (!success) { - req.flash('message', {error: true, text: 'Invalid or expired activation token.'}) - } else { - req.flash('message', {error: false, text: 'Your account has been activated! You may now log in.'}) - } - - res.redirect('/login') -})) - router.use('/api', apiRouter) router.use('/admin', adminRouter) router.use('/mc', mcRouter) diff --git a/server/routes/minecraft.js b/server/routes/minecraft.js index d31aae5..6795589 100644 --- a/server/routes/minecraft.js +++ b/server/routes/minecraft.js @@ -1,4 +1,5 @@ import express from 'express' + import ensureLogin from '../../scripts/ensureLogin' import wrap from '../../scripts/asyncRoute' import Minecraft from '../api/minecraft' diff --git a/server/routes/oauth2.js b/server/routes/oauth2.js index 073d636..a29920f 100644 --- a/server/routes/oauth2.js +++ b/server/routes/oauth2.js @@ -1,5 +1,6 @@ import express from 'express' -import uapi from '../api' + +import UAPI from '../api' import OAuth2 from '../api/oauth2' import RateLimit from 'express-rate-limit' import wrap from '../../scripts/asyncRoute' @@ -31,7 +32,7 @@ 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) + let user = await UAPI.User.get(accessToken.user_id) if (!user) { return res.status(404).jsonp({ diff --git a/server/server.js b/server/server.js index 215ffcd..82a8549 100644 --- a/server/server.js +++ b/server/server.js @@ -68,7 +68,7 @@ module.exports = (args) => { app.set('views', path.join(__dirname, '../views')) if (args.dev) { - console.log('Worker is in development mode') + console.warn('Worker is in development mode') // Dev logger const morgan = require('morgan')