Commit cf862486 authored by Jarrod's avatar Jarrod 💬

Update for auth endpoints

parent 9a39fb06
module.exports = {
friendlyName: 'Complete password reset',
description: 'Update a users password',
inputs: {
e: {
type: 'string',
required: true
},
t: {
type: 'string',
required: true
},
password: {
type: 'string',
required: true,
minLength: 5
}
},
exits: {
},
fn: async function (inputs, exits) {
const crypto = require('crypto')
const jwt = require('machinepack-jwt-ng')
const config = sails.config.custom
let user = await User.findOne({
where: { passwordResetVerificationToken: inputs.t }
});
if (!user) { return this.res.badRequest('Invalid token') }
const emailHash = crypto
.createHash('md5')
.update(user.email, 'utf8')
.digest('hex')
if (inputs.e !== emailHash) {
if (!user) { return this.res.badRequest('Invalid token') }
}
user = await User.updateOne({ id: user.id })
.set({
password: inputs.password,
passwordResetVerificationToken: null,
emailVerified: true
})
let token = await jwt.sign({ user: User.pickToken(user) }, config.jwt)
this.res.set('X-Token-Update', token)
return exits.success({ status: 'ok' })
}
}
module.exports = {
friendlyName: 'Confirm a recovery token/email pair exists',
description: 'I guess this is used on the "reset password" page to push the user away if given an invalid token/email',
inputs: {
e: {
type: 'string',
required: true
},
t: {
type: 'string',
required: true
}
},
exits: {
},
fn: async function (inputs, exits) {
const crypto = require('crypto')
let user = await User.findOne({
passwordResetVerificationToken: inputs.t
})
if (!user) { return this.res.badRequest('Invalid token') }
let emailHash = crypto
.createHash('md5')
.update(user.emailAddress, 'utf8')
.digest('hex')
if (inputs.e !== emailHash) {
if (!user) { return this.res.badRequest({ error: 'Invalid token' }); }
}
return exits.success({ status: 'ok' })
}
}
module.exports = {
friendlyName: 'Login',
description: 'Check a given username/password combination and return a JWT if a user match is found',
inputs: {
username: {
type: 'string',
required: true
},
password: {
type: 'string',
required: true
}
},
exits: {
},
fn: async function ({ username, password }) {
const jwt = require('machinepack-jwt-ng')
const bcrypt = require('bcrypt')
let error = this.res.__('auth.invalid') // 'Invalid username or password'
let user = await User.findOne({ username })
.catch((err) => {
sails.log.error('Unable to check if signup users username is taken!', err.stack)
})
if (!user) {
return this.res.badRequest(error)
}
//check password
if (!await bcrypt.compare(password, user.password)) {
return this.res.badRequest(error)
}
let token = await jwt.sign({ user: User.pickToken(user) }, sails.config.custom.jwt)
return {
user,
token
}
}
}
module.exports = {
friendlyName: 'Login',
description: 'Check a given username/password combination and return a JWT if a user match is found',
inputs: {
email: {
required: true,
isEmail: true
}
},
exits: {
},
fn: async function ({ email }, exits) {
const crypto = require('crypto')
const uuid = require('uuid/v4')
let user = await User.findOne({ email })
if (!user) {
return this.res.badRequest('No such user')
}
let emailHash = crypto
.createHash('md5')
.update(user.email, 'utf8')
.digest('hex')
// Generate a reset token & store in db
let token = uuid()
user = await User
.updateOne({ id: user.id })
.set({ passwordResetVerificationToken: token })
// Send the email
let sent = await sails.helpers.ahoy.email.send('password-recovery', user.email, {
subject: 'Password Recovery',
//...
user,
email: emailHash,
token
});
// Return some success status
if (sent) {
return { status: 'ok' }
}
return this.res.serverError('Failed')
}
}
module.exports = {
friendlyName: 'Re-send verification email',
description: '',
inputs: {
},
exits: {
},
fn: async function (inputs, exits) {
const uuid = require('uuid/v4')
let user = this.req.me
if (!user) { return this.res.unauthorized() }
let emailHash = crypto
.createHash('md5')
.update(user.emailChangeCandidate ? user.emailChangeCandidate : user.emailAddress, 'utf8')
.digest('hex')
if (!user.emailVerificationToken) {
user = await User.updateOne({ id: user.id })
.set({
emailVerificationToken: uuid()
// TODO:: emailVerificationTokenExpiresAt: new Date(now - 2-24hrs?)
})
}
// Send the email (async!)
try {
await sails.helpers.ahoy.email.send('email-verification', user.emailChangeCandidate ? user.emailChangeCandidate : user.email, {
subject: `Verify your email for ${sails.config.custom.site.name}`,
user: user,
email: emailHash,
token: user.emailVerificationToken
})
} catch (err) {
sails.log.error(`Unable to resend verification email`, err)
return res.serverError('Unable to send verification email')
}
return exits.success({ status: 'ok' })
}
}
module.exports = {
friendlyName: 'Register a user account',
description: '',
inputs: {
username: {
type: 'string',
required: true,
minLength: 1,
},
email: {
type: 'string',
required: true,
isEmail: true
},
password: {
type: 'string',
required: true,
minLength: 5,
}
},
exits: {
created: {
statusCode: 201
}
},
fn: async function (data, exits) {
const crypto = require('crypto')
const jwt = require('machinepack-jwt-ng')
const uuid = require('uuid/v4')
const config = sails.config.custom
let errors = {}
let existingUser = await User.count({ username: data.username })
if (existingUser) {
errors.username = ['That username is already taken']
}
existingUser = await User.count({ emailAddress: data.email })
if (existingUser) {
errors.email = ['That email address is already in use']
}
if (!_.isEmpty(errors)) {
return this.res.badRequest({
error: 'There was a problem with the entered values',
fields: errors
})
}
let newUser
data.emailAddress = data.email
data.emailVerificationToken = uuid()
try {
newUser = await User.create(data).fetch()
} catch (err) {
return this.res.serverError(err)
}
let emailHash = crypto
.createHash('md5')
.update(newUser.emailAddress, 'utf8')
.digest('hex')
// Send the email
await sails.helpers.ahoy.email.send('new-account', newUser.emailAddress, {
subject: `Welcome to ${config.site.name}`,
user: newUser,
email: emailHash,
token: data.emailVerificationToken
}).then(() => { /* do nothing */ })
let token = await jwt.sign({ user: User.pickToken(newUser) }, config.jwt)
exits.created({ user: newUser, token })
}
}
module.exports = {
friendlyName: 'Verify user email account',
inputs: {
e: {
type: 'string',
required: true
},
t: {
type: 'string',
required: true
}
},
exits: {
},
fn: async function (inputs, exits) {
const crypto = require('crypto')
const jwt = require('machinepack-jwt-ng')
const config = sails.config.custom
let user = await User.findOne({
emailVerificationToken: inputs.t
})
if (!user) { return this.res.badRequest('Invalid token') }
let emailHash = crypto
.createHash('md5')
.update(user.emailAddress, 'utf8')
.digest('hex')
if (inputs.e !== emailHash) {
return this.res.badRequest('Invalid token')
}
let update = {}
update.emailVerificationToken = null
update.emailVerified = true
user = await User
.updateOne({ id: user.id })
.set(update)
let token = await jwt.sign({ user: User.pickToken(user) }, config.jwt)
this.res.set('X-Token-Update', token)
return exits.success({ status: 'ok' })
}
}
const render = async function(template, args = { }) {
const config = sails.config.custom
// Determine appropriate email layout and template to use.
const path = require('path')
const emailTemplatePath = path.join('email/', template);
let layout = path.relative(path.dirname(emailTemplatePath), path.resolve('layouts/', args.layout || 'layout'));
// Compile HTML template.
// > Note that we set the layout, provide access to core `url` package (for
// > building links and image srcs, etc.), and also provide access to core
// > `util` package (for dumping debug data in internal emails).
var htmlEmailContents = await sails.renderView(
emailTemplatePath,
_.extend({ layout }, { data: args, site: config.site, title: args.subject || '' }, args.templateData)
)
.intercept((err)=>{
err.message =
'Could not compile view template.\n'+
'(Usually, this means the provided data is invalid, or missing a piece.)\n'+
'Details:\n'+
err.message;
return err;
});
return htmlEmailContents
}
async function send (template, to, args) {
const config = sails.config.custom
const mailgun = require('mailgun-js')( config.mailgun )
return new Promise(async (resolve, reject) => {
let html = await render(`${template}`, args)
let text = undefined
var data = {
from: args.from || config.site.emailFrom,
subject: args.subject || `Notification from ${config.site.name}`,
to,
text,
html
};
if (sails.config.environment !== 'production') {
console.log(`~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`)
console.log(`Not sending email as not in production mode`)
console.log(`I would have sent the following message:`)
console.log(` To: ${data.to}`)
console.log(` Subject: ${data.subject}`)
console.log(` Message:\n${data.html}`)
return resolve(true)
}
mailgun.messages().send(data, function (err, body) {
if (err) {
console.log('Failed to send email', err.stack)
return resolve(false)
}
resolve(true)
});
});
}
module.exports = send
......@@ -11,6 +11,15 @@ module.exports = new Pluggable({
dir: __dirname,
actions: {
// Auth/registration
'POST /auth/login': { action: 'auth/login', ratelimit: '???' },
'POST /auth/sign-up': { action: 'auth/sign-up', ratelimit: '???' },
'POST /auth/recovery': { action: 'auth/recovery', ratelimit: '???' },
'POST /auth/confirm-recovery': { action: 'auth/confirm-recovery', ratelimit: '???' },
'POST /auth/complete-recovery': { action: 'auth/complete-recovery', ratelimit: '???' },
'POST /auth/verify-email': { action: 'auth/verify-email', ratelimit: '???' },
'POST /auth/resend-verification': { action: 'auth/resend-verification', ratelimit: '???' },
'GET /ahoy.js': { action: 'get-ahoyjs', cache: 30 },
'GET /apidoc': {
action: 'apidoc',
......@@ -25,6 +34,12 @@ module.exports = new Pluggable({
},
},
helpers: {
email: {
send: require('./helpers/email/send')
}
},
async initialize () {
sails.after('lifted', () => {
// Attach custom methods to `res` object like `res.notFound('message')`
......@@ -37,9 +52,11 @@ module.exports = new Pluggable({
sails.middleware.responses[mw[0]] = function (arg) {
const { req, res } = this
const message = !arg ? mw[3] : _.isString(arg) ? arg : _.isObject(arg) && arg.message ? arg.message : mw[3]
const fields = mw[1] === 400 && arg.fields ? arg.fields : undefined
res.status(mw[1]).send({
error: mw[2],
message
message,
fields
}).end()
}
}
......
......@@ -5,25 +5,6 @@ module.exports = async function (req, res, next) {
const logLevel = sails.hooks.ahoy.logLevel || 'info'
sails.log[logLevel](`Running ahoy user middleware`)
// Shim the old `req.user` usage so it works, but logs a stack trace to the console
// TODO: Remove theeeees
function shimUserProperty () {
Object.defineProperty(req, 'user', {
get () {
sails.log.warn((new Error(`Deprecated usage of "req.user" seen`)).stack)
return req.me
}
})
}
if (req.user) {
sails.log.warn(`Using user from upstream (old express app)`)
req.me = req.user
shimUserProperty()
return next()
}
shimUserProperty()
let token
try {
token = await jwt.fromRequest(req.headers, {
......
......@@ -4,6 +4,7 @@
*
* A user who can log in to this application.
*/
const bcrypt = require('bcrypt');
module.exports = {
......@@ -54,6 +55,19 @@ module.exports = {
defaultsTo: false
},
emailVerificationToken: {
columnName: 'email_verification_token',
type: 'string',
allowNull: true,
description: 'A unique token used to verify the user\'s email address'
},
emailVerificationTokenExpiresAt: {
columnName: 'email_verification_token_expires',
type: 'ref',
columnType: 'timestamptz default now()'
},
permissions: {
type: 'ref',
columnType: 'text[]',
......@@ -134,11 +148,31 @@ without necessarily having a billing card.`
*/
},
async beforeCreate (values, next) {
if (values.password) {
values.password = await bcrypt.hash(values.password, 12)
}
next()
},
async beforeUpdate (values, next) {
if (values.password) {
values.password = await bcrypt.hash(values.password, 12)
}
next()
},
customToJSON () {
return _.omit(this, ['password'])
},
pickCore (user) {
return _.pick(user, ['id', 'username', 'avatar'])
return _.pick(user, ['id', 'username', 'avatar', 'createdAt'])
},
pickToken (user) {
return _.pick(user, ['id', 'username', 'avatar', 'isAdmin', 'emailVerified'])
}
}
{
"name": "@nahanil/ahoy",
"version": "0.0.2",
"version": "0.0.3",
"description": "",
"main": "lib/index.js",
"scripts": {
......@@ -15,8 +15,11 @@
"author": "Jarrod Linahan <jarrod@linahan.id.au>",
"license": "MIT",
"dependencies": {
"bcrypt": "^3.0.6",
"machinepack-jwt-ng": "git+ssh://git@git.carrotlabs.net:jarrod/machinepack-jwt-ng.git",
"memory-cache": "^0.2.0"
"mailgun-js": "^0.22.0",
"memory-cache": "^0.2.0",
"uuid": "^3.3.2"
},
"devDependencies": {
"jest": "^24.8.0",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment