From 6e13dce8452b794e919749cf52b38b33e76674eb Mon Sep 17 00:00:00 2001 From: Evert Date: Thu, 30 Nov 2017 23:13:14 +0200 Subject: [PATCH] A whole load of stuff - Added password reset, changed error returns to throws and some other things --- package-lock.json | 26 ++++- package.json | 1 + scripts/existsSync.js | 17 ---- scripts/logger.js | 6 +- server/api/admin.js | 24 ++--- server/api/external.js | 13 ++- server/api/image.js | 14 +-- server/api/index.js | 84 ++++++++++++--- server/api/news.js | 4 +- server/routes/admin.js | 24 +---- server/routes/api.js | 27 ++--- server/routes/index.js | 147 ++++++++++++++++++++------- src/script/admin.js | 15 +-- src/style/main.styl | 5 + templates/reset_password/html.pug | 6 ++ templates/reset_password/subject.pug | 1 + views/user/login.pug | 2 + views/user/password_new.pug | 2 - views/user/reset_password.pug | 23 +++++ 19 files changed, 300 insertions(+), 141 deletions(-) delete mode 100644 scripts/existsSync.js create mode 100644 templates/reset_password/html.pug create mode 100644 templates/reset_password/subject.pug create mode 100644 views/user/reset_password.pug diff --git a/package-lock.json b/package-lock.json index aefe5e1..e3ca338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2104,6 +2104,16 @@ "version": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=" }, + "fs-extra": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz", + "integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2269,8 +2279,7 @@ "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, "graceful-readlink": { "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", @@ -2822,6 +2831,14 @@ "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "4.1.11" + } + }, "jsonify": { "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" @@ -5082,6 +5099,11 @@ "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", "dev": true }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, "unpipe": { "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" diff --git a/package.json b/package.json index 6aae3eb..8b3a1f3 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "express": "^4.15.3", "express-rate-limit": "^2.9.0", "express-session": "^1.15.3", + "fs-extra": "^4.0.2", "gm": "^1.23.0", "knex": "^0.13.0", "multiparty": "^4.1.3", diff --git a/scripts/existsSync.js b/scripts/existsSync.js deleted file mode 100644 index 42adacd..0000000 --- a/scripts/existsSync.js +++ /dev/null @@ -1,17 +0,0 @@ -import path from 'path' -import Promise from 'bluebird' -import fs from 'fs' - -const access = Promise.promisify(fs.access) - -async function exists (fpath) { - try { - await access(path.resolve(fpath)) - } catch (e) { - return false - } - - return true -} - -module.exports = exists diff --git a/scripts/logger.js b/scripts/logger.js index 516d645..1baa486 100644 --- a/scripts/logger.js +++ b/scripts/logger.js @@ -1,9 +1,7 @@ import config from './load-config' import path from 'path' import util from 'util' - -import Promise from 'bluebird' -const fs = Promise.promisifyAll(require('fs')) +import fs from 'fs-extra' let lfs @@ -57,7 +55,7 @@ async function initializeLogger () { let logPath = path.resolve(config.logger.file) try { - await fs.accessAsync(logPath, fs.W_OK) + await fs.access(logPath, fs.W_OK) lfs = fs.createWriteStream(logPath, {flags: 'a'}) } catch (e) { lfs = null diff --git a/server/api/admin.js b/server/api/admin.js index cbc9b98..e2ec69a 100644 --- a/server/api/admin.js +++ b/server/api/admin.js @@ -84,7 +84,7 @@ const API = { getAllUsers: async function (page, adminId) { let count = await Models.User.query().count('id as ids') if (!count.length || !count[0]['ids'] || isNaN(page)) { - return { error: 'No users found' } + throw new Error('No users found') } count = count[0].ids @@ -108,7 +108,7 @@ const API = { getAllClients: async function (page) { let count = await Models.OAuth2Client.query().count('id as ids') if (!count.length || !count[0]['ids'] || isNaN(page)) { - return { error: 'No clients found' } + return { error: 'No clients' } } count = count[0].ids @@ -141,7 +141,7 @@ const API = { ] data = dataFilter(data, fields, ['scope', 'verified']) - if (!data) return { error: 'Missing fields' } + if (!data) throw new Error('Missing fields') data.verified = (data.verified != null ? 1 : 0) @@ -149,20 +149,20 @@ const API = { await Models.OAuth2Client.query().patchAndFetchById(id, data) await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id) } catch (e) { - return { error: 'No such client' } + throw new Error('No such client') } return {} }, // Create a new secret for a client newSecret: async function (id) { - if (isNaN(id)) return { error: 'Invalid client ID' } + if (isNaN(id)) throw new Error('Invalid client ID') let secret = Users.Hash(16) try { await Models.OAuth2Client.query().patchAndFetchById(id, {secret: secret}) } catch (e) { - return { error: 'No such client' } + throw new Error('No such client') } return {} @@ -174,7 +174,7 @@ const API = { ] data = dataFilter(data, fields, ['scope']) - if (!data) return { error: 'Missing fields' } + if (!data) throw new Error('Missing fields') let obj = Object.assign({ secret: Users.Hash(16), @@ -187,7 +187,7 @@ const API = { }, // Remove a client and all associated data removeClient: async function (id) { - if (isNaN(id)) return {error: 'Invalid ID number'} + if (isNaN(id)) throw new Error('Invalid ID number') await Models.OAuth2Client.query().delete().where('id', id) await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id) await Models.OAuth2AccessToken.query().delete().where('client_id', id) @@ -198,7 +198,7 @@ const API = { getAllBans: async function (page) { let count = await Models.Ban.query().count('id as ids') if (!count.length || !count[0]['ids'] || isNaN(page)) { - return {error: 'No bans on record'} + throw new Error('No bans on record') } count = count[0].ids @@ -225,12 +225,12 @@ const API = { addBan: async function (data, adminId) { let user = await Users.User.get(parseInt(data.user_id)) - if (!user) return {error: 'No such user.'} - if (user.id === adminId) return {error: 'Cannot ban yourself!'} + if (!user) throw new Error('No such user.') + if (user.id === adminId) throw new Error('Cannot ban yourself!') let admin = await Users.User.get(adminId) - if (user.nw_privilege > admin.nw_privilege) return {error: 'Cannot ban user.'} + if (user.nw_privilege > admin.nw_privilege) throw new Error('Cannot ban user.') let banAdd = { reason: data.reason || 'Unspecified ban', diff --git a/server/api/external.js b/server/api/external.js index 5921124..7419d3e 100644 --- a/server/api/external.js +++ b/server/api/external.js @@ -79,12 +79,11 @@ const API = { } // Check if the email given to us is already registered, if so, - // associate an external node with the user bearing the email + // tell them to log in first. if (udataLimited.email && udataLimited.email !== '') { let getByEmail = await UAPI.User.get(udataLimited.email) if (getByEmail) { - await API.Common.new(service, identifier, getByEmail) - return {error: null, user: getByEmail} + throw new Error('An user with this email address is already registered, but this external account is are not linked. If you wish to link the account, please log in first.') } } @@ -158,8 +157,12 @@ const API = { } } - let newUser = await API.Common.newUser(identifier, uid, newUData) - if (!newUser) return {error: 'Failed to create user.'} + let newUser + try { + newUser = await API.Common.newUser(identifier, uid, newUData) + } catch (e) { + return {error: e.message} + } return {error: null, user: newUser} } diff --git a/server/api/image.js b/server/api/image.js index a2e8b19..ce89916 100644 --- a/server/api/image.js +++ b/server/api/image.js @@ -6,7 +6,7 @@ import Promise from 'bluebird' import http from '../../scripts/http' -const fs = Promise.promisifyAll(require('fs')) +const fs = require('fs-extra') const uploads = path.join(__dirname, '../../', 'usercontent') const images = path.join(uploads, 'images') @@ -49,8 +49,8 @@ function saneFields (fields) { } async function bailOut (file, error) { - await fs.unlinkAsync(file) - return { error: error } + await fs.unlink(file) + throw new Error(error) } async function imageBase64 (baseObj) { @@ -68,7 +68,7 @@ async function imageBase64 (baseObj) { let fpath = path.join(images, imageName) try { - await fs.writeFileAsync(fpath, imgData.data) + await fs.writeFile(fpath, imgData.data) } catch (e) { console.error(e) return null @@ -93,11 +93,11 @@ async function downloadImage (imgUrl, designation) { return null } - return {fileName: imageName} + return imageName } async function uploadImage (identifier, fields, files) { - if (!files.image) return {error: 'No image file'} + if (!files.image) throw new Error('No image file') let file = files.image[0] if (file.size > maxFileSize) return bailOut(file.path, 'Image is too large! 1 MB max') @@ -158,7 +158,7 @@ async function uploadImage (identifier, fields, files) { }) }) - await fs.unlinkAsync(file) + await fs.unlink(file) } catch (e) { console.error(e) return bailOut(file, 'An error occured while cropping.') diff --git a/server/api/index.js b/server/api/index.js index 028531d..3c29c76 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -2,16 +2,13 @@ import path from 'path' import cprog from 'child_process' import config from '../../scripts/load-config' import http from '../../scripts/http' -import exists from '../../scripts/existsSync' 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 Promise from 'bluebird' -const fs = Promise.promisifyAll(require('fs')) +import fs from 'fs-extra' 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,}))$/ @@ -178,7 +175,7 @@ const API = { }, update: async function (user, data) { user = await API.User.ensureObject(user) - if (!user) return {error: 'No such user.'} + if (!user) throw new Error('No such user.') data = Object.assign({ updated_at: new Date() @@ -191,20 +188,20 @@ const API = { let uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images') let pathOf = path.join(uploadsDir, fileName) - if (!await exists(pathOf)) { - return {error: 'No such file'} + if (!await fs.exists(pathOf)) { + throw new Error('No such file') } // Delete previous upload if (user.avatar_file != null) { let file = path.join(uploadsDir, user.avatar_file) - if (await exists(file)) { - await fs.unlinkAsync(file) + if (await fs.exists(file)) { + await fs.unlink(file) } } await API.User.update(user, {avatar_file: fileName}) - return { file: fileName } + return fileName }, removeAvatar: async function (user) { user = await API.User.ensureObject(user, ['avatar_file']) @@ -212,7 +209,7 @@ const API = { if (!user.avatar_file) return {} let file = path.join(uploadsDir, user.avatar_file) - if (await exists(file)) { + if (await fs.exists(file)) { await fs.unlinkAsync(file) } @@ -252,7 +249,7 @@ const API = { return bcryptTask({task: 'compare', password: password, hash: user.password}) }, activationToken: async function (token) { - let getToken = await models.Token.query().where('token', token) + let getToken = await models.Token.query().where('token', token).andWhere('type', 1) if (!getToken || !getToken.length) return false let user = await API.User.get(getToken[0].user_id) @@ -359,12 +356,12 @@ const API = { let userTest = await API.User.get(regdata.username) if (userTest) { - return {error: 'This username is already taken!'} + throw new Error('This username is already taken!') } let emailTest = await API.User.get(regdata.email) if (emailTest) { - return {error: 'This email address is already registered!'} + throw new Error('This email address is already registered!') } // Create user @@ -393,11 +390,66 @@ const API = { } catch (e) { console.error(e) await models.User.query().delete().where('id', user.id) - return {error: 'Invalid email address!'} + throw new Error('Invalid email address!') } } - return {error: null, user: user} + return user + } + }, + Reset: { + reset: async function (email, passRequired = true) { + let emailEnabled = config.email && config.email.enabled + + if (!emailEnabled) throw new Error('Cannot reset password.') + + let user = await API.User.get(email) + if (!user) throw new Error('This email address does not match any user in our database.') + if (!user.password && passRequired) throw new Error('The user associated with this email address has used an external website to log in, thus the password cannot be reset.') + + let resetToken = API.Hash(16) + await models.Token.query().insert({ + expires_at: new Date(Date.now() + 86400000), // 1 day + token: resetToken, + user_id: user.id, + type: 2 + }) + + // Send Reset Email + console.debug('Reset token:', resetToken) + if (email) { + try { + let em = await emailer.pushMail('reset_password', user.email, { + domain: config.server.domain, + display_name: user.display_name, + reset_token: resetToken + }) + + console.debug(em) + } catch (e) { + console.error(e) + throw new Error('Invalid email address!') + } + } + + return resetToken + }, + resetToken: async function (token) { + let getToken = await models.Token.query().where('token', token).andWhere('type', 2) + if (!getToken || !getToken.length) return null + + let user = await API.User.get(getToken[0].user_id) + if (!user) return null + + return user + }, + changePassword: async function (user, password, token) { + let hashed = await API.User.Register.hashPassword(password) + + await models.User.query().patchAndFetchById(user.id, {password: hashed, updated_at: new Date()}) + await models.Token.query().delete().where('token', token) + + return true } }, OAuth2: { diff --git a/server/api/news.js b/server/api/news.js index 4814d1d..3ccb650 100644 --- a/server/api/news.js +++ b/server/api/news.js @@ -53,7 +53,7 @@ const News = { if (page < 1) page = 1 if (!count.length || !count[0]['ids'] || isNaN(page)) { - return {error: 'No articles found'} + return { error: 'No news' } } count = count[0].ids @@ -100,7 +100,7 @@ const News = { } let result = await Models.News.query().patchAndFetchById(id, patch) - if (!result) return {error: 'Something went wrong.'} + if (!result) throw new Error('Something went wrong.') return {} } } diff --git a/server/routes/admin.js b/server/routes/admin.js index 088a80f..2a898c4 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -128,10 +128,7 @@ apiRouter.post('/client/new', wrap(async (req, res) => { return res.status(400).jsonp({error: 'Invalid session'}) } - let update = await API.createClient(req.body, req.session.user) - if (update.error) { - return res.status(400).jsonp({error: update.error}) - } + await API.createClient(req.body, req.session.user) res.status(204).end() })) @@ -145,10 +142,7 @@ apiRouter.post('/client/update', wrap(async (req, res) => { return res.status(400).jsonp({error: 'Invalid session'}) } - let update = await API.updateClient(id, req.body) - if (update.error) { - return res.status(400).jsonp({error: update.error}) - } + await API.updateClient(id, req.body) res.status(204).end() })) @@ -160,9 +154,6 @@ apiRouter.post('/client/new_secret/:id', wrap(async (req, res) => { } let client = await API.newSecret(id) - if (client.error) { - return res.status(400).jsonp({error: client.error}) - } res.jsonp(client) })) @@ -174,9 +165,6 @@ apiRouter.post('/client/delete/:id', wrap(async (req, res) => { } let client = await API.removeClient(id) - if (client.error) { - return res.status(400).jsonp({error: client.error}) - } res.jsonp(client) })) @@ -203,9 +191,6 @@ apiRouter.post('/ban/pardon/:id', wrap(async (req, res) => { } let ban = await API.removeBan(id) - if (ban.error) { - return res.status(400).jsonp({error: ban.error}) - } res.jsonp(ban) })) @@ -217,16 +202,13 @@ apiRouter.post('/ban', wrap(async (req, res) => { } let result = await API.addBan(req.body, req.session.user.id) - if (result.error) { - return res.status(400).jsonp({error: result.error}) - } res.jsonp(result) })) apiRouter.use((err, req, res, next) => { console.error(err) - return res.status(500).jsonp({error: 'Internal server error'}) + return res.status(400).jsonp({error: err.message}) }) router.use('/api', apiRouter) diff --git a/server/routes/api.js b/server/routes/api.js index 1ff26e3..2f0e207 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -317,9 +317,10 @@ router.post('/news/edit/:id', wrap(async (req, res, next) => { return res.status(400).jsonp({error: 'Content is required.'}) } - let result = await News.edit(id, req.body) - if (result.error) { - return res.status(400).jsonp({error: result.error}) + try { + await News.edit(id, req.body) + } catch (e) { + return res.status(400).jsonp({error: e.message}) } res.status(204).end() @@ -363,19 +364,19 @@ async function promiseForm (req) { router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => { if (!req.session.user) return next() let data = await promiseForm(req) - let result = await Image.uploadImage(req.session.user.username, data.fields, data.files) - if (result.error) { - return res.status(400).jsonp({error: result.error}) + let avatarFile + + try { + let result = await Image.uploadImage(req.session.user.username, data.fields, data.files) + + avatarFile = await API.User.changeAvatar(req.session.user, result.file) + } catch (e) { + return res.status(400).jsonp({error: e.message}) } - let avatarUpdate = await API.User.changeAvatar(req.session.user, result.file) - if (avatarUpdate.error) { - return res.status(400).jsonp({error: avatarUpdate.error}) - } - - if (avatarUpdate.file) { - req.session.user.avatar_file = avatarUpdate.file + if (avatarFile) { + req.session.user.avatar_file = avatarFile } res.status(200).jsonp({}) diff --git a/server/routes/index.js b/server/routes/index.js index 75d156e..f8a7f94 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,10 +1,9 @@ -import fs from 'fs' +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 exists from '../../scripts/existsSync' import wrap from '../../scripts/asyncRoute' import http from '../../scripts/http' import API from '../api' @@ -135,6 +134,28 @@ function formKeep (req, res, next) { next() } +// Password reset request endpoint +router.get('/login/reset', extraButtons, (req, res) => { + if (req.session.user) return redirectLogin(req, res) + + res.render('user/reset_password', {sent: req.query.success != null}) +}) + +// Password reset endpoint (emailed link) +router.get('/reset/:token', wrap(async (req, res) => { + if (req.session.user) return res.redirect('/login') + let token = req.params.token + let success = await API.User.Reset.resetToken(token) + + if (!success) { + req.flash('message', {error: true, text: 'Invalid or expired reset token.'}) + res.redirect('/login') + return + } + + res.render('user/password_new', {token: true}) +})) + router.get('/login', extraButtons, (req, res) => { if (req.session.user) return redirectLogin(req, res) if (req.query.returnTo) { @@ -383,6 +404,61 @@ router.post('/login', accountLimiter, csrfValidation, wrap(async (req, res, next res.redirect(uri) })) +// 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.') + } + + let email = req.body.email + let validEmail = await API.User.Register.validateEmail(email) + if (!validEmail) { + return formError(req, res, 'You need to enter a valid email address.') + } + + try { + await API.User.Reset.reset(email) + res.redirect('/login/reset?success=true') + } catch (e) { + return formError(req, res, e.message) + } +})) + +// Password reset endpoint (emailed link) +router.post('/reset/:token', csrfValidation, wrap(async (req, res) => { + if (req.session.user) return res.redirect('/login') + let token = req.params.token + let user = await API.User.Reset.resetToken(token) + + if (!user) { + req.flash('message', {error: true, text: 'Invalid or expired reset token.'}) + res.redirect('/login') + return + } + + // 4th Check: Password length + let password = req.body.password + if (!password || password.length < 8) { + return formError(req, res, 'Invalid password! Please use at least 8 characters!') + } + + // 5th Check: Password match + let passwordAgain = req.body.password_repeat + if (!passwordAgain || password !== passwordAgain) { + return formError(req, res, 'Passwords do not match!') + } + + try { + await API.User.Reset.changePassword(user, password, token) + + req.flash('message', {error: false, text: 'Your password has been changed successfully. You may now log in!'}) + res.redirect('/login') + } catch (e) { + return formError(req, res, e.message) + } +})) + // Protected & Limited resource: Account registration router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, next) => { if (req.session.user) return next() @@ -448,18 +524,19 @@ router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, n // Hash the password let hash = await API.User.Register.hashPassword(password) + let newUser // Attempt to create the user - let newUser = await API.User.Register.newAccount({ - username: username, - display_name: cleanString(displayName), - password: hash, - email: email, - ip_address: req.realIP - }) - - if (!newUser || newUser.error != null) { - return formError(req, res, newUser.error) + try { + newUser = await API.User.Register.newAccount({ + username: username, + display_name: cleanString(displayName), + password: hash, + email: email, + ip_address: req.realIP + }) + } catch (e) { + return formError(req, res, e.message) } // Do not include activation link message when the user is already activated @@ -492,12 +569,12 @@ router.post('/user/manage', csrfValidation, wrap(async (req, res, next) => { return res.redirect('/user/manage') } - let success = await API.User.update(req.session.user, { - display_name: displayName - }) - - if (success.error) { - return formError(req, res, success.error) + try { + await API.User.update(req.session.user, { + display_name: displayName + }) + } catch (e) { + return formError(req, res, e.message) } req.session.user.display_name = displayName @@ -532,12 +609,12 @@ router.post('/user/manage/password', accountLimiter, csrfValidation, wrap(async password = await API.User.Register.hashPassword(password) - let success = await API.User.update(user, { - password: password - }) - - if (success.error) { - return formError(req, res, success.error) + try { + await API.User.update(user, { + password: password + }) + } catch (e) { + return formError(req, res, e.message) } console.warn('[SECURITY AUDIT] User \'%s\' password has been changed from %s', user.username, req.realIP) @@ -591,12 +668,12 @@ router.post('/user/manage/email', accountLimiter, csrfValidation, wrap(async (re return formError(req, res, 'This email is already taken.') } - let success = await API.User.update(user, { - email: newEmail - }) - - if (success.error) { - return formError(req, res, success.error) + try { + await API.User.update(user, { + email: newEmail + }) + } catch (e) { + return formError(req, res, e.message) } // TODO: Send necessary emails @@ -618,7 +695,7 @@ router.post('/user/manage/email', accountLimiter, csrfValidation, wrap(async (re const docsDir = path.join(__dirname, '../../documents') router.get('/docs/:name', wrap(async (req, res, next) => { let doc = path.join(docsDir, req.params.name + '.html') - if (!await exists(docsDir) || !await exists(doc)) { + if (!await fs.exists(docsDir) || !await fs.exists(doc)) { return next() } @@ -650,9 +727,11 @@ router.post('/news/compose', newsPrivilege, csrfValidation, wrap(async (req, res return formError(req, res, 'Required fields missing!') } - let result = await News.compose(req.session.user, req.body) - if (result.error) { - return formError(req, res, result.error) + let result + try { + result = await News.compose(req.session.user, req.body) + } catch (e) { + return formError(req, res, e.message) } res.redirect('/news/' + result.id + '-' + result.slug) diff --git a/src/script/admin.js b/src/script/admin.js index 854eb1a..356c245 100644 --- a/src/script/admin.js +++ b/src/script/admin.js @@ -12,15 +12,20 @@ function buildTemplateScript (id, ctx) { function paginationButton (pages) { var html = '
' html += 'Page ' + pages.page + ' of ' + pages.pages + '' + if (pages.page > 1) { html += '
Previous
' } + for (var i = 0; i < pages.pages; i++) { - html += '
' + (i + 1) + '
' + html += '
' + (i + 1) + '
' } + if (pages.pages > pages.page) { html += '
Next
' } + + html += '(' + pages.total + ' Total Entries)' html += '
' return html } @@ -271,11 +276,9 @@ $(document).ready(function () { setInterval(function () { $.get({ - url: '/admin/access', - success: function (data) { - if (data && data.access) return - window.location.reload() - } + url: '/admin/access' + }).fail(function () { + window.location.reload() }) }, 30000) }) diff --git a/src/style/main.styl b/src/style/main.styl index b6c8af1..4b18b9d 100644 --- a/src/style/main.styl +++ b/src/style/main.styl @@ -206,6 +206,8 @@ input:not([type="submit"]) font-size: 120% margin: 10px 0 cursor: pointer + &.active + background-color: #ddd .button display: inline-block @@ -254,6 +256,9 @@ input:not([type="submit"]) .pagenum display: inline-block padding: 10px + &.total + color: #a9a9a9 + font-style: italic .dlbtn display: block diff --git a/templates/reset_password/html.pug b/templates/reset_password/html.pug new file mode 100644 index 0000000..e4c8fce --- /dev/null +++ b/templates/reset_password/html.pug @@ -0,0 +1,6 @@ +h1 Hello, #{display_name}! +p You've requested to reset your password on Icy Network. +p Click on or copy the following link into your URL bar in order to reset your Icy Network account password: +a.activate(href=domain + "/reset/" + reset_token, target="_blank", rel="nofollow")= domain + "/reset/" + reset_token +p If you did not request for a password reset on Icy Network, please ignore this email. +small This email has been sent to you because of an action performed on the IcyNet.eu website. diff --git a/templates/reset_password/subject.pug b/templates/reset_password/subject.pug new file mode 100644 index 0000000..85b8d0b --- /dev/null +++ b/templates/reset_password/subject.pug @@ -0,0 +1 @@ +|Icy Network - Password reset request diff --git a/views/user/login.pug b/views/user/login.pug index fe6e6a8..5c9a099 100644 --- a/views/user/login.pug +++ b/views/user/login.pug @@ -23,5 +23,7 @@ block body input(type="password", name="password", id="password") input(type="submit", value="Log in") a#create(href="/register") Create an account + span.divider · + a#create(href="/login/reset") Forgot password? .right include ../includes/external.pug diff --git a/views/user/password_new.pug b/views/user/password_new.pug index bbc09a7..87d0c09 100644 --- a/views/user/password_new.pug +++ b/views/user/password_new.pug @@ -19,8 +19,6 @@ block body if !token label(for="password_old") Current Password input(type="password", name="password_old", id="password_old") - else - input(type="hidden", name="token", value=token) label(for="password") New Password input(type="password", name="password", id="password") label(for="password_repeat") Repeat New Password diff --git a/views/user/reset_password.pug b/views/user/reset_password.pug new file mode 100644 index 0000000..854a1d5 --- /dev/null +++ b/views/user/reset_password.pug @@ -0,0 +1,23 @@ +extends ../layout.pug +block title + |Icy Network - Reset Password + +block body + .wrapper + .boxcont + .box#totpcheck + h1 Reset your password + p Enter your email below in order to reset your password. + if message.text + if message.error + .message.error + span #{message.text} + else + .message + span #{message.text} + if !sent + form#loginForm(method="POST", action=post) + input(type="hidden", name="csrf", value=csrf) + label(for="email") Email Address + input(type="email", name="email", id="email") + input(type="submit", value="Continue")