A whole load of stuff - Added password reset, changed error returns to throws and some other things

This commit is contained in:
Evert Prants 2017-11-30 23:13:14 +02:00
parent b4b88a5657
commit 6e13dce845
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
19 changed files with 300 additions and 141 deletions

26
package-lock.json generated
View File

@ -2104,6 +2104,16 @@
"version": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", "version": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
"integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=" "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": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2269,8 +2279,7 @@
"graceful-fs": { "graceful-fs": {
"version": "4.1.11", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
"dev": true
}, },
"graceful-readlink": { "graceful-readlink": {
"version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "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", "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" "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": { "jsonify": {
"version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
@ -5082,6 +5099,11 @@
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
"dev": true "dev": true
}, },
"universalify": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"unpipe": { "unpipe": {
"version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="

View File

@ -41,6 +41,7 @@
"express": "^4.15.3", "express": "^4.15.3",
"express-rate-limit": "^2.9.0", "express-rate-limit": "^2.9.0",
"express-session": "^1.15.3", "express-session": "^1.15.3",
"fs-extra": "^4.0.2",
"gm": "^1.23.0", "gm": "^1.23.0",
"knex": "^0.13.0", "knex": "^0.13.0",
"multiparty": "^4.1.3", "multiparty": "^4.1.3",

View File

@ -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

View File

@ -1,9 +1,7 @@
import config from './load-config' import config from './load-config'
import path from 'path' import path from 'path'
import util from 'util' import util from 'util'
import fs from 'fs-extra'
import Promise from 'bluebird'
const fs = Promise.promisifyAll(require('fs'))
let lfs let lfs
@ -57,7 +55,7 @@ async function initializeLogger () {
let logPath = path.resolve(config.logger.file) let logPath = path.resolve(config.logger.file)
try { try {
await fs.accessAsync(logPath, fs.W_OK) await fs.access(logPath, fs.W_OK)
lfs = fs.createWriteStream(logPath, {flags: 'a'}) lfs = fs.createWriteStream(logPath, {flags: 'a'})
} catch (e) { } catch (e) {
lfs = null lfs = null

View File

@ -84,7 +84,7 @@ const API = {
getAllUsers: async function (page, adminId) { getAllUsers: async function (page, adminId) {
let count = await Models.User.query().count('id as ids') let count = await Models.User.query().count('id as ids')
if (!count.length || !count[0]['ids'] || isNaN(page)) { if (!count.length || !count[0]['ids'] || isNaN(page)) {
return { error: 'No users found' } throw new Error('No users found')
} }
count = count[0].ids count = count[0].ids
@ -108,7 +108,7 @@ const API = {
getAllClients: async function (page) { getAllClients: async function (page) {
let count = await Models.OAuth2Client.query().count('id as ids') let count = await Models.OAuth2Client.query().count('id as ids')
if (!count.length || !count[0]['ids'] || isNaN(page)) { if (!count.length || !count[0]['ids'] || isNaN(page)) {
return { error: 'No clients found' } return { error: 'No clients' }
} }
count = count[0].ids count = count[0].ids
@ -141,7 +141,7 @@ const API = {
] ]
data = dataFilter(data, fields, ['scope', 'verified']) 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) data.verified = (data.verified != null ? 1 : 0)
@ -149,20 +149,20 @@ const API = {
await Models.OAuth2Client.query().patchAndFetchById(id, data) await Models.OAuth2Client.query().patchAndFetchById(id, data)
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id) await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
} catch (e) { } catch (e) {
return { error: 'No such client' } throw new Error('No such client')
} }
return {} return {}
}, },
// Create a new secret for a client // Create a new secret for a client
newSecret: async function (id) { 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) let secret = Users.Hash(16)
try { try {
await Models.OAuth2Client.query().patchAndFetchById(id, {secret: secret}) await Models.OAuth2Client.query().patchAndFetchById(id, {secret: secret})
} catch (e) { } catch (e) {
return { error: 'No such client' } throw new Error('No such client')
} }
return {} return {}
@ -174,7 +174,7 @@ const API = {
] ]
data = dataFilter(data, fields, ['scope']) data = dataFilter(data, fields, ['scope'])
if (!data) return { error: 'Missing fields' } if (!data) throw new Error('Missing fields')
let obj = Object.assign({ let obj = Object.assign({
secret: Users.Hash(16), secret: Users.Hash(16),
@ -187,7 +187,7 @@ const API = {
}, },
// Remove a client and all associated data // Remove a client and all associated data
removeClient: async function (id) { 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.OAuth2Client.query().delete().where('id', id)
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id) await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
await Models.OAuth2AccessToken.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) { getAllBans: async function (page) {
let count = await Models.Ban.query().count('id as ids') let count = await Models.Ban.query().count('id as ids')
if (!count.length || !count[0]['ids'] || isNaN(page)) { 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 count = count[0].ids
@ -225,12 +225,12 @@ const API = {
addBan: async function (data, adminId) { addBan: async function (data, adminId) {
let user = await Users.User.get(parseInt(data.user_id)) let user = await Users.User.get(parseInt(data.user_id))
if (!user) return {error: 'No such user.'} if (!user) throw new Error('No such user.')
if (user.id === adminId) return {error: 'Cannot ban yourself!'} if (user.id === adminId) throw new Error('Cannot ban yourself!')
let admin = await Users.User.get(adminId) 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 = { let banAdd = {
reason: data.reason || 'Unspecified ban', reason: data.reason || 'Unspecified ban',

View File

@ -79,12 +79,11 @@ const API = {
} }
// Check if the email given to us is already registered, if so, // 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 !== '') { if (udataLimited.email && udataLimited.email !== '') {
let getByEmail = await UAPI.User.get(udataLimited.email) let getByEmail = await UAPI.User.get(udataLimited.email)
if (getByEmail) { if (getByEmail) {
await API.Common.new(service, identifier, 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.')
return {error: null, user: getByEmail}
} }
} }
@ -158,8 +157,12 @@ const API = {
} }
} }
let newUser = await API.Common.newUser(identifier, uid, newUData) let newUser
if (!newUser) return {error: 'Failed to create user.'} try {
newUser = await API.Common.newUser(identifier, uid, newUData)
} catch (e) {
return {error: e.message}
}
return {error: null, user: newUser} return {error: null, user: newUser}
} }

View File

@ -6,7 +6,7 @@ import Promise from 'bluebird'
import http from '../../scripts/http' import http from '../../scripts/http'
const fs = Promise.promisifyAll(require('fs')) const fs = require('fs-extra')
const uploads = path.join(__dirname, '../../', 'usercontent') const uploads = path.join(__dirname, '../../', 'usercontent')
const images = path.join(uploads, 'images') const images = path.join(uploads, 'images')
@ -49,8 +49,8 @@ function saneFields (fields) {
} }
async function bailOut (file, error) { async function bailOut (file, error) {
await fs.unlinkAsync(file) await fs.unlink(file)
return { error: error } throw new Error(error)
} }
async function imageBase64 (baseObj) { async function imageBase64 (baseObj) {
@ -68,7 +68,7 @@ async function imageBase64 (baseObj) {
let fpath = path.join(images, imageName) let fpath = path.join(images, imageName)
try { try {
await fs.writeFileAsync(fpath, imgData.data) await fs.writeFile(fpath, imgData.data)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
return null return null
@ -93,11 +93,11 @@ async function downloadImage (imgUrl, designation) {
return null return null
} }
return {fileName: imageName} return imageName
} }
async function uploadImage (identifier, fields, files) { 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] let file = files.image[0]
if (file.size > maxFileSize) return bailOut(file.path, 'Image is too large! 1 MB max') 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) { } catch (e) {
console.error(e) console.error(e)
return bailOut(file, 'An error occured while cropping.') return bailOut(file, 'An error occured while cropping.')

View File

@ -2,16 +2,13 @@ import path from 'path'
import cprog from 'child_process' import cprog from 'child_process'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import http from '../../scripts/http' import http from '../../scripts/http'
import exists from '../../scripts/existsSync'
import models from './models' import models from './models'
import crypto from 'crypto' import crypto from 'crypto'
import notp from 'notp' import notp from 'notp'
import base32 from 'thirty-two' import base32 from 'thirty-two'
import emailer from './emailer' import emailer from './emailer'
import uuidV1 from 'uuid/v1' import uuidV1 from 'uuid/v1'
import fs from 'fs-extra'
import Promise from 'bluebird'
const fs = Promise.promisifyAll(require('fs'))
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,}))$/ 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) { update: async function (user, data) {
user = await API.User.ensureObject(user) user = await API.User.ensureObject(user)
if (!user) return {error: 'No such user.'} if (!user) throw new Error('No such user.')
data = Object.assign({ data = Object.assign({
updated_at: new Date() updated_at: new Date()
@ -191,20 +188,20 @@ const API = {
let uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images') let uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images')
let pathOf = path.join(uploadsDir, fileName) let pathOf = path.join(uploadsDir, fileName)
if (!await exists(pathOf)) { if (!await fs.exists(pathOf)) {
return {error: 'No such file'} throw new Error('No such file')
} }
// Delete previous upload // Delete previous upload
if (user.avatar_file != null) { if (user.avatar_file != null) {
let file = path.join(uploadsDir, user.avatar_file) let file = path.join(uploadsDir, user.avatar_file)
if (await exists(file)) { if (await fs.exists(file)) {
await fs.unlinkAsync(file) await fs.unlink(file)
} }
} }
await API.User.update(user, {avatar_file: fileName}) await API.User.update(user, {avatar_file: fileName})
return { file: fileName } return fileName
}, },
removeAvatar: async function (user) { removeAvatar: async function (user) {
user = await API.User.ensureObject(user, ['avatar_file']) user = await API.User.ensureObject(user, ['avatar_file'])
@ -212,7 +209,7 @@ const API = {
if (!user.avatar_file) return {} if (!user.avatar_file) return {}
let file = path.join(uploadsDir, user.avatar_file) let file = path.join(uploadsDir, user.avatar_file)
if (await exists(file)) { if (await fs.exists(file)) {
await fs.unlinkAsync(file) await fs.unlinkAsync(file)
} }
@ -252,7 +249,7 @@ const API = {
return bcryptTask({task: 'compare', password: password, hash: user.password}) return bcryptTask({task: 'compare', password: password, hash: user.password})
}, },
activationToken: async function (token) { 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 if (!getToken || !getToken.length) return false
let user = await API.User.get(getToken[0].user_id) let user = await API.User.get(getToken[0].user_id)
@ -359,12 +356,12 @@ const API = {
let userTest = await API.User.get(regdata.username) let userTest = await API.User.get(regdata.username)
if (userTest) { 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) let emailTest = await API.User.get(regdata.email)
if (emailTest) { if (emailTest) {
return {error: 'This email address is already registered!'} throw new Error('This email address is already registered!')
} }
// Create user // Create user
@ -393,11 +390,66 @@ const API = {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
await models.User.query().delete().where('id', user.id) 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: { OAuth2: {

View File

@ -53,7 +53,7 @@ const News = {
if (page < 1) page = 1 if (page < 1) page = 1
if (!count.length || !count[0]['ids'] || isNaN(page)) { if (!count.length || !count[0]['ids'] || isNaN(page)) {
return {error: 'No articles found'} return { error: 'No news' }
} }
count = count[0].ids count = count[0].ids
@ -100,7 +100,7 @@ const News = {
} }
let result = await Models.News.query().patchAndFetchById(id, patch) 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 {} return {}
} }
} }

View File

@ -128,10 +128,7 @@ apiRouter.post('/client/new', wrap(async (req, res) => {
return res.status(400).jsonp({error: 'Invalid session'}) return res.status(400).jsonp({error: 'Invalid session'})
} }
let update = await API.createClient(req.body, req.session.user) await API.createClient(req.body, req.session.user)
if (update.error) {
return res.status(400).jsonp({error: update.error})
}
res.status(204).end() res.status(204).end()
})) }))
@ -145,10 +142,7 @@ apiRouter.post('/client/update', wrap(async (req, res) => {
return res.status(400).jsonp({error: 'Invalid session'}) return res.status(400).jsonp({error: 'Invalid session'})
} }
let update = await API.updateClient(id, req.body) await API.updateClient(id, req.body)
if (update.error) {
return res.status(400).jsonp({error: update.error})
}
res.status(204).end() res.status(204).end()
})) }))
@ -160,9 +154,6 @@ apiRouter.post('/client/new_secret/:id', wrap(async (req, res) => {
} }
let client = await API.newSecret(id) let client = await API.newSecret(id)
if (client.error) {
return res.status(400).jsonp({error: client.error})
}
res.jsonp(client) res.jsonp(client)
})) }))
@ -174,9 +165,6 @@ apiRouter.post('/client/delete/:id', wrap(async (req, res) => {
} }
let client = await API.removeClient(id) let client = await API.removeClient(id)
if (client.error) {
return res.status(400).jsonp({error: client.error})
}
res.jsonp(client) res.jsonp(client)
})) }))
@ -203,9 +191,6 @@ apiRouter.post('/ban/pardon/:id', wrap(async (req, res) => {
} }
let ban = await API.removeBan(id) let ban = await API.removeBan(id)
if (ban.error) {
return res.status(400).jsonp({error: ban.error})
}
res.jsonp(ban) 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) 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) res.jsonp(result)
})) }))
apiRouter.use((err, req, res, next) => { apiRouter.use((err, req, res, next) => {
console.error(err) console.error(err)
return res.status(500).jsonp({error: 'Internal server error'}) return res.status(400).jsonp({error: err.message})
}) })
router.use('/api', apiRouter) router.use('/api', apiRouter)

View File

@ -317,9 +317,10 @@ router.post('/news/edit/:id', wrap(async (req, res, next) => {
return res.status(400).jsonp({error: 'Content is required.'}) return res.status(400).jsonp({error: 'Content is required.'})
} }
let result = await News.edit(id, req.body) try {
if (result.error) { await News.edit(id, req.body)
return res.status(400).jsonp({error: result.error}) } catch (e) {
return res.status(400).jsonp({error: e.message})
} }
res.status(204).end() res.status(204).end()
@ -363,19 +364,19 @@ async function promiseForm (req) {
router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => { router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => {
if (!req.session.user) return next() if (!req.session.user) return next()
let data = await promiseForm(req) let data = await promiseForm(req)
let result = await Image.uploadImage(req.session.user.username, data.fields, data.files)
if (result.error) { let avatarFile
return res.status(400).jsonp({error: result.error})
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 (avatarFile) {
if (avatarUpdate.error) { req.session.user.avatar_file = avatarFile
return res.status(400).jsonp({error: avatarUpdate.error})
}
if (avatarUpdate.file) {
req.session.user.avatar_file = avatarUpdate.file
} }
res.status(200).jsonp({}) res.status(200).jsonp({})

View File

@ -1,10 +1,9 @@
import fs from 'fs' import fs from 'fs-extra'
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 ensureLogin from '../../scripts/ensureLogin' import ensureLogin from '../../scripts/ensureLogin'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import exists from '../../scripts/existsSync'
import wrap from '../../scripts/asyncRoute' import wrap from '../../scripts/asyncRoute'
import http from '../../scripts/http' import http from '../../scripts/http'
import API from '../api' import API from '../api'
@ -135,6 +134,28 @@ function formKeep (req, res, next) {
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) => { router.get('/login', extraButtons, (req, res) => {
if (req.session.user) return redirectLogin(req, res) if (req.session.user) return redirectLogin(req, res)
if (req.query.returnTo) { if (req.query.returnTo) {
@ -383,6 +404,61 @@ router.post('/login', accountLimiter, csrfValidation, wrap(async (req, res, next
res.redirect(uri) 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 // Protected & Limited resource: Account registration
router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, next) => { router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, next) => {
if (req.session.user) return next() if (req.session.user) return next()
@ -448,18 +524,19 @@ router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, n
// Hash the password // Hash the password
let hash = await API.User.Register.hashPassword(password) let hash = await API.User.Register.hashPassword(password)
let newUser
// Attempt to create the user // Attempt to create the user
let newUser = await API.User.Register.newAccount({ try {
username: username, newUser = await API.User.Register.newAccount({
display_name: cleanString(displayName), username: username,
password: hash, display_name: cleanString(displayName),
email: email, password: hash,
ip_address: req.realIP email: email,
}) ip_address: req.realIP
})
if (!newUser || newUser.error != null) { } catch (e) {
return formError(req, res, newUser.error) return formError(req, res, e.message)
} }
// Do not include activation link message when the user is already activated // 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') return res.redirect('/user/manage')
} }
let success = await API.User.update(req.session.user, { try {
display_name: displayName await API.User.update(req.session.user, {
}) display_name: displayName
})
if (success.error) { } catch (e) {
return formError(req, res, success.error) return formError(req, res, e.message)
} }
req.session.user.display_name = displayName 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) password = await API.User.Register.hashPassword(password)
let success = await API.User.update(user, { try {
password: password await API.User.update(user, {
}) password: password
})
if (success.error) { } catch (e) {
return formError(req, res, success.error) return formError(req, res, e.message)
} }
console.warn('[SECURITY AUDIT] User \'%s\' password has been changed from %s', user.username, req.realIP) 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.') return formError(req, res, 'This email is already taken.')
} }
let success = await API.User.update(user, { try {
email: newEmail await API.User.update(user, {
}) email: newEmail
})
if (success.error) { } catch (e) {
return formError(req, res, success.error) return formError(req, res, e.message)
} }
// TODO: Send necessary emails // TODO: Send necessary emails
@ -618,7 +695,7 @@ router.post('/user/manage/email', accountLimiter, csrfValidation, wrap(async (re
const docsDir = path.join(__dirname, '../../documents') const docsDir = path.join(__dirname, '../../documents')
router.get('/docs/:name', wrap(async (req, res, next) => { router.get('/docs/:name', wrap(async (req, res, next) => {
let doc = path.join(docsDir, req.params.name + '.html') 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() return next()
} }
@ -650,9 +727,11 @@ router.post('/news/compose', newsPrivilege, csrfValidation, wrap(async (req, res
return formError(req, res, 'Required fields missing!') return formError(req, res, 'Required fields missing!')
} }
let result = await News.compose(req.session.user, req.body) let result
if (result.error) { try {
return formError(req, res, result.error) result = await News.compose(req.session.user, req.body)
} catch (e) {
return formError(req, res, e.message)
} }
res.redirect('/news/' + result.id + '-' + result.slug) res.redirect('/news/' + result.id + '-' + result.slug)

View File

@ -12,15 +12,20 @@ function buildTemplateScript (id, ctx) {
function paginationButton (pages) { function paginationButton (pages) {
var html = '<div class="pgn">' var html = '<div class="pgn">'
html += '<span class="pagenum">Page ' + pages.page + ' of ' + pages.pages + '</span>' html += '<span class="pagenum">Page ' + pages.page + ' of ' + pages.pages + '</span>'
if (pages.page > 1) { if (pages.page > 1) {
html += '<div class="button" data-page="' + (pages.page - 1) + '">Previous</div>' html += '<div class="button" data-page="' + (pages.page - 1) + '">Previous</div>'
} }
for (var i = 0; i < pages.pages; i++) { for (var i = 0; i < pages.pages; i++) {
html += '<div class="button" data-page="' + (i + 1) + '">' + (i + 1) + '</div>' html += '<div class="button' + (i + 1 === pages.page ? ' active' : '') + '" data-page="' + (i + 1) + '">' + (i + 1) + '</div>'
} }
if (pages.pages > pages.page) { if (pages.pages > pages.page) {
html += '<div class="button" data-page="' + (pages.page + 1) + '">Next</div>' html += '<div class="button" data-page="' + (pages.page + 1) + '">Next</div>'
} }
html += '<span class="pagenum total">(' + pages.total + ' Total Entries)</span>'
html += '</div>' html += '</div>'
return html return html
} }
@ -271,11 +276,9 @@ $(document).ready(function () {
setInterval(function () { setInterval(function () {
$.get({ $.get({
url: '/admin/access', url: '/admin/access'
success: function (data) { }).fail(function () {
if (data && data.access) return window.location.reload()
window.location.reload()
}
}) })
}, 30000) }, 30000)
}) })

View File

@ -206,6 +206,8 @@ input:not([type="submit"])
font-size: 120% font-size: 120%
margin: 10px 0 margin: 10px 0
cursor: pointer cursor: pointer
&.active
background-color: #ddd
.button .button
display: inline-block display: inline-block
@ -254,6 +256,9 @@ input:not([type="submit"])
.pagenum .pagenum
display: inline-block display: inline-block
padding: 10px padding: 10px
&.total
color: #a9a9a9
font-style: italic
.dlbtn .dlbtn
display: block display: block

View File

@ -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.

View File

@ -0,0 +1 @@
|Icy Network - Password reset request

View File

@ -23,5 +23,7 @@ block body
input(type="password", name="password", id="password") input(type="password", name="password", id="password")
input(type="submit", value="Log in") input(type="submit", value="Log in")
a#create(href="/register") Create an account a#create(href="/register") Create an account
span.divider &middot;
a#create(href="/login/reset") Forgot password?
.right .right
include ../includes/external.pug include ../includes/external.pug

View File

@ -19,8 +19,6 @@ block body
if !token if !token
label(for="password_old") Current Password label(for="password_old") Current Password
input(type="password", name="password_old", id="password_old") input(type="password", name="password_old", id="password_old")
else
input(type="hidden", name="token", value=token)
label(for="password") New Password label(for="password") New Password
input(type="password", name="password", id="password") input(type="password", name="password", id="password")
label(for="password_repeat") Repeat New Password label(for="password_repeat") Repeat New Password

View File

@ -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")