diff --git a/server/routes/index.js b/server/routes/index.js index 544813e..8b2024b 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -85,6 +85,12 @@ function extraButtons (req, res, next) { next() } +function ensureLogin (req, res, next) { + if (req.session.user) return next() + req.session.redirectUri = req.originalUrl + res.redirect('/login') +} + router.get('/login', extraButtons, (req, res) => { if (req.session.user) { let uri = '/' @@ -118,8 +124,7 @@ router.get('/register', extraButtons, (req, res) => { res.render('register') }) -router.get('/user/two-factor', wrap(async (req, res) => { - if (!req.session.user) return res.redirect('/login') +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('/') @@ -129,8 +134,7 @@ router.get('/user/two-factor', wrap(async (req, res) => { res.render('totp', { uri: newToken }) })) -router.get('/user/two-factor/disable', wrap(async (req, res) => { - if (!req.session.user) return res.redirect('/login') +router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => { let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user) if (!twoFaEnabled) return res.redirect('/') @@ -141,9 +145,7 @@ router.get('/login/verify', (req, res) => { res.render('totp-check') }) -router.get('/user/manage', wrap(async (req, res) => { - if (!req.session.user) return res.redirect('/login') - +router.get('/user/manage', ensureLogin, wrap(async (req, res) => { let totpEnabled = false let socialStatus = await API.User.socialStatus(req.session.user) @@ -178,15 +180,11 @@ router.get('/user/manage', wrap(async (req, res) => { res.render('settings', {totp: totpEnabled, password: socialStatus.password}) })) -router.get('/user/manage/password', wrap(async (req, res) => { - if (!req.session.user) return res.redirect('/login') - +router.get('/user/manage/password', ensureLogin, wrap(async (req, res) => { res.render('password_new') })) -router.get('/user/manage/email', wrap(async (req, res) => { - if (!req.session.user) return res.redirect('/login') - +router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => { let obfuscated = req.session.user.email if (obfuscated) { let split = obfuscated.split('@') @@ -216,7 +214,16 @@ function formError (req, res, error, redirect) { res.redirect(redirect || parseurl(req).path) } -router.post('/user/two-factor', wrap(async (req, res) => { +function cleanString (input) { + let output = '' + for (let i = 0; i < input.length; i++) { + output += input.charCodeAt(i) <= 127 ? input.charAt(i) : '' + } + return output +} + +router.post('/user/two-factor', 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.') } @@ -233,7 +240,8 @@ router.post('/user/two-factor', wrap(async (req, res) => { res.redirect('/') })) -router.post('/user/two-factor/disable', wrap(async (req, res) => { +router.post('/user/two-factor/disable', wrap(async (req, res, next) => { + if (!req.session.user) return next() if (req.body.csrf !== req.session.csrf) { return formError(req, res, 'Invalid session! Try reloading the page.') } @@ -250,7 +258,8 @@ router.post('/user/two-factor/disable', wrap(async (req, res) => { res.redirect('/') })) -router.post('/login/verify', wrap(async (req, res) => { +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') if (!req.body.code && !req.body.recovery) { return formError(req, res, 'You need to enter the code.') @@ -291,8 +300,9 @@ router.post('/login/verify', wrap(async (req, res) => { res.redirect(uri) })) -router.post('/login', wrap(async (req, res) => { - if (!req.body.username || !req.body.password) { +router.post('/login', 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') } @@ -303,6 +313,8 @@ router.post('/login', wrap(async (req, res) => { let user = await API.User.get(req.body.username) if (!user) return formError(req, res, 'Invalid username or password.') + if (!user.password || user.password === '') return formError(req, res, 'Please log in using the buttons on the right.') + let pwMatch = await API.User.Login.password(user, req.body.password) if (!pwMatch) return formError(req, res, 'Invalid username or password.') @@ -333,7 +345,8 @@ router.post('/login', wrap(async (req, res) => { res.redirect(uri) })) -router.post('/register', accountLimiter, wrap(async (req, res) => { +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) { return formError(req, res, 'Please fill in all the fields.') } @@ -344,7 +357,7 @@ router.post('/register', accountLimiter, wrap(async (req, res) => { // 1st Check: Username Characters and length let username = req.body.username - if (!username || !username.match(/^([\w-]{3,26})$/i)) { + if (!username || !username.match(/^([\w-_]{3,26})$/i)) { return formError(req, res, 'Invalid username! Must be between 3-26 characters and composed of alphanumeric characters!') } @@ -398,7 +411,7 @@ router.post('/register', accountLimiter, wrap(async (req, res) => { // Attempt to create the user let newUser = await API.User.Register.newAccount({ username: username, - display_name: displayName, + display_name: cleanString(displayName), password: hash, email: email, ip_address: req.realIP @@ -428,6 +441,8 @@ router.post('/user/manage', wrap(async (req, res, next) => { return formError(req, res, 'Invalid display name!') } + displayName = cleanString(displayName) + // No change if (displayName === req.session.user.display_name) { return res.redirect('/user/manage') @@ -458,7 +473,8 @@ router.post('/user/manage/password', wrap(async (req, res, next) => { return formError(req, res, 'Please enter your current password.') } - let passwordMatch = await API.User.Login.password(req.session.user, req.body.password_old) + let user = req.session.user + let passwordMatch = await API.User.Login.password(user, req.body.password_old) if (!passwordMatch) { return formError(req, res, 'The password you provided is incorrect.') } @@ -475,7 +491,7 @@ router.post('/user/manage/password', wrap(async (req, res, next) => { password = await API.User.Register.hashPassword(password) - let success = await API.User.update(req.session.user, { + let success = await API.User.update(user, { password: password }) @@ -483,7 +499,6 @@ router.post('/user/manage/password', wrap(async (req, res, next) => { return formError(req, res, success.error) } - let user = req.session.user console.warn('[SECURITY AUDIT] User \'%s\' password has been changed from %s', user.username, req.realIP) if (config.email && config.email.enabled) { @@ -557,7 +572,11 @@ router.get('/docs/:name', (req, res, next) => { return next() } - doc = fs.readFileSync(doc, {encoding: 'utf8'}) + try { + doc = fs.readFileSync(doc, {encoding: 'utf8'}) + } catch (e) { + return next(e) + } res.render('document', {doc: doc}) }) @@ -587,6 +606,12 @@ router.get('/news/', wrap(async (req, res) => { res.render('news', {news: news}) })) +router.get('/partials/:view', wrap(async (req, res, next) => { + if (!req.params.view) return next() + + res.render('partials/' + req.params.view) +})) + /* ========= OTHER @@ -614,12 +639,34 @@ router.get('/activate/:token', wrap(async (req, res) => { router.use('/api', apiRouter) +/* + NO ROUTES BEYOND THIS POINT +*/ + +// Handle 'Failed to lookup view' errors +router.use((err, req, res, next) => { + if (err && err.stack) { + if (err.stack.indexOf('Failed to lookup view') !== -1) { + return next() // To 404 + } + } + + next(err) // To error handler +}) + +// 404 - last route router.use((req, res) => { res.status(404).render('404') }) +// Error handler router.use((err, req, res, next) => { console.error(err) + + if (process.env.NODE_ENV !== 'production') { + return res.end(err.stack) + } + next() }) diff --git a/server/routes/oauth2.js b/server/routes/oauth2.js index fec7965..6b70ee1 100644 --- a/server/routes/oauth2.js +++ b/server/routes/oauth2.js @@ -2,7 +2,6 @@ import express from 'express' import uapi from '../api' import OAuth2 from '../api/oauth2' import RateLimit from 'express-rate-limit' -import config from '../../scripts/load-config' import wrap from '../../scripts/asyncRoute' let router = express.Router() @@ -19,12 +18,9 @@ let oauthLimiter = new RateLimit({ router.use(oauthLimiter) function ensureLoggedIn (req, res, next) { - if (req.session.user) { - next() - } else { - req.session.redirectUri = req.originalUrl - res.redirect('/login') - } + if (req.session.user) return next() + req.session.redirectUri = req.originalUrl + res.redirect('/login') } router.use('/authorize', ensureLoggedIn, oauth.controller.authorization) diff --git a/src/script/main.js b/src/script/main.js index 34315c7..384e00e 100644 --- a/src/script/main.js +++ b/src/script/main.js @@ -15,6 +15,37 @@ $(document).ready(function () { } } + window.Dialog = $('#dialog') + window.Dialog.open = function (title, content, pad) { + $('#dialog #title').text(title) + if (pad) { + content = '
' + content + '
' + } + $('#dialog #content').html(content) + $('#dialog').fadeIn() + } + + window.Dialog.close = function () { + $('#dialog').fadeOut('fast', function () { + $('#dialog #content').html('') + }) + } + + window.Dialog.openPartial = function (title, partial) { + $.get({ + url: '/partials/' + partial, + success: function (html) { + window.Dialog.open(title, html, false) + } + }).fail(function (e) { + console.error(e) + }) + } + + $('#dialog #close').click(function (e) { + window.Dialog.close() + }) + if (window.location.hash) { var locha = window.location.hash if ($(locha).length) { diff --git a/src/style/main.styl b/src/style/main.styl index 35ba716..e1e36eb 100644 --- a/src/style/main.styl +++ b/src/style/main.styl @@ -441,6 +441,50 @@ input.invalid .option display: block +.dialog-drop + display: block + position: fixed + display: none + top: 0 + left: 0 + bottom: 0 + right: 0 + background-color: rgba(56, 56, 56, 0.8) + z-index: 10 + .dialog + min-width: 400px + width: fit-content + max-width: 50vw + min-height: 180px + height: fit-content + background-color: #fff + margin: auto + margin-top: 5% + transition: all 0.1s linear + overflow: hidden + .pad + padding: 5px + .head + height: 40px + background-color: #e6e6e6 + box-shadow: 0px 3px #ddd + overflow: hidden + position: relative + #title + max-width: 80% + text-overflow: ellipsis + overflow: hidden + white-space: nowrap + margin: 12px + display: block + #close + position: absolute + top: 6px + right: 5px + color: #ff2525 + cursor: pointer + padding: 5px + @media all and (max-width: 800px) .navigator padding: 0 10px diff --git a/views/layout.pug b/views/layout.pug index 3950944..ec820d0 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -15,6 +15,14 @@ html .logo .part1 Icy .part2 Network + block dialog + .dialog-drop#dialog + .dialog + .head + #title + #close + i.fa.fa-fw.fa-times + .content#content block nav .anchor nav.navigator diff --git a/views/partials/avatar.pug b/views/partials/avatar.pug new file mode 100644 index 0000000..84f6aa2 --- /dev/null +++ b/views/partials/avatar.pug @@ -0,0 +1 @@ +img(src=user.avatar_file)