wip(backend): Singup workflow and tests
This commit is contained in:
parent
0eaccf7b18
commit
efe9e6c24d
5 changed files with 113 additions and 60 deletions
|
@ -6,6 +6,7 @@
|
||||||
*.mustache
|
*.mustache
|
||||||
*.png
|
*.png
|
||||||
*.svg
|
*.svg
|
||||||
|
*.prisma
|
||||||
yarn.lock
|
yarn.lock
|
||||||
.husky/pre-commit
|
.husky/pre-commit
|
||||||
.prettierignore
|
.prettierignore
|
||||||
|
|
|
@ -32,7 +32,7 @@ model User {
|
||||||
email String
|
email String
|
||||||
ihash String
|
ihash String
|
||||||
initial String
|
initial String
|
||||||
lastlogin DateTime?
|
lastLogin DateTime?
|
||||||
newsletter Boolean @default(false)
|
newsletter Boolean @default(false)
|
||||||
password String
|
password String
|
||||||
patron Int @default(0)
|
patron Int @default(0)
|
||||||
|
|
|
@ -31,7 +31,7 @@ const config = {
|
||||||
secretOrKey: process.env.API_ENC_KEY,
|
secretOrKey: process.env.API_ENC_KEY,
|
||||||
issuer: process.env.API_JWT_ISSUER,
|
issuer: process.env.API_JWT_ISSUER,
|
||||||
audience: process.env.API_JWT_ISSUER,
|
audience: process.env.API_JWT_ISSUER,
|
||||||
expiresIn: process.env.API_JWT_EXPIRY || '36 days',
|
expiresIn: process.env.API_JWT_EXPIRY || '7d',
|
||||||
},
|
},
|
||||||
languages: ['en', 'de', 'es', 'fr', 'nl'],
|
languages: ['en', 'de', 'es', 'fr', 'nl'],
|
||||||
aws: {
|
aws: {
|
||||||
|
|
|
@ -14,9 +14,35 @@ import { emailTemplate } from '../utils/email.mjs'
|
||||||
* So here's a bunch of helper methods that expect a user object
|
* So here's a bunch of helper methods that expect a user object
|
||||||
* as input
|
* as input
|
||||||
*/
|
*/
|
||||||
const asAccount = user => {
|
const asAccount = (user, decrypt) => ({
|
||||||
return user
|
id: user.id,
|
||||||
}
|
consent: user.consent,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
data: user.data,
|
||||||
|
email: decrypt(user.email),
|
||||||
|
initial: decrypt(user.initial),
|
||||||
|
lastLogin: user.lastLogin,
|
||||||
|
newsletter: user.newsletter,
|
||||||
|
patron: user.patron,
|
||||||
|
role: user.role,
|
||||||
|
status: user.status,
|
||||||
|
updatedAt: user.updatedAt,
|
||||||
|
username: user.username,
|
||||||
|
})
|
||||||
|
|
||||||
|
const getToken = (user, config) =>
|
||||||
|
jwt.sign(
|
||||||
|
{
|
||||||
|
_id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
status: user.status,
|
||||||
|
aud: config.jwt.audience,
|
||||||
|
iss: config.jwt.issuer,
|
||||||
|
},
|
||||||
|
config.jwt.secretOrKey,
|
||||||
|
{ expiresIn: config.jwt.expiresIn }
|
||||||
|
)
|
||||||
|
|
||||||
// We'll send this result unless it goes ok
|
// We'll send this result unless it goes ok
|
||||||
const result = 'error'
|
const result = 'error'
|
||||||
|
@ -30,10 +56,10 @@ export function UserController() {}
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
UserController.prototype.signup = async (req, res, tools) => {
|
UserController.prototype.signup = async (req, res, tools) => {
|
||||||
if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result})
|
if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result })
|
||||||
if (!req.body.email) return res.status(400).json({ error: 'emailMissing', result})
|
if (!req.body.email) return res.status(400).json({ error: 'emailMissing', result })
|
||||||
if (!req.body.password) return res.status(400).json({ error: 'passwordMissing', result})
|
if (!req.body.password) return res.status(400).json({ error: 'passwordMissing', result })
|
||||||
if (!req.body.language) return res.status(400).json({ error: 'languageMissing', result})
|
if (!req.body.language) return res.status(400).json({ error: 'languageMissing', result })
|
||||||
|
|
||||||
// Destructure what we need from tools
|
// Destructure what we need from tools
|
||||||
const { prisma, config, encrypt, email } = tools
|
const { prisma, config, encrypt, email } = tools
|
||||||
|
@ -41,7 +67,7 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
// Requests looks ok - does the user exist?
|
// Requests looks ok - does the user exist?
|
||||||
const emailhash = hash(clean(req.body.email))
|
const emailhash = hash(clean(req.body.email))
|
||||||
if (await prisma.user.findUnique({ where: { ehash: emailhash } })) {
|
if (await prisma.user.findUnique({ where: { ehash: emailhash } })) {
|
||||||
return res.status(400).json({ error: 'emailExists', result})
|
return res.status(400).json({ error: 'emailExists', result })
|
||||||
}
|
}
|
||||||
// It does not. Creating user entry
|
// It does not. Creating user entry
|
||||||
let record
|
let record
|
||||||
|
@ -54,12 +80,12 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
ihash: emailhash,
|
ihash: emailhash,
|
||||||
initial: encrypt(clean(req.body.email)),
|
initial: encrypt(clean(req.body.email)),
|
||||||
password: asJson(hashPassword(req.body.password)),
|
password: asJson(hashPassword(req.body.password)),
|
||||||
username: randomString() // Temporary username,
|
username: randomString(), // Temporary username,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn(err, 'Could not create user record')
|
log.warn(err, 'Could not create user record')
|
||||||
return res.status(500).send({ error: 'createAccountFailed', result})
|
return res.status(500).send({ error: 'createAccountFailed', result })
|
||||||
}
|
}
|
||||||
// Now set username to user-ID
|
// Now set username to user-ID
|
||||||
let updated
|
let updated
|
||||||
|
@ -67,12 +93,12 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
updated = await prisma.user.update({
|
updated = await prisma.user.update({
|
||||||
where: { id: record.id },
|
where: { id: record.id },
|
||||||
data: {
|
data: {
|
||||||
username: `user-${record.id}`
|
username: `user-${record.id}`,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn(err, 'Could not update username on created user record')
|
log.warn(err, 'Could not update username on created user record')
|
||||||
return res.status(500).send({result: 'error', error: 'updateCreatedAccountUsernameFailed'})
|
return res.status(500).send({ result: 'error', error: 'updateCreatedAccountUsernameFailed' })
|
||||||
}
|
}
|
||||||
log.info({ user: updated.id }, 'Account created')
|
log.info({ user: updated.id }, 'Account created')
|
||||||
|
|
||||||
|
@ -86,14 +112,13 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
language: req.body.language,
|
language: req.body.language,
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
id: record.id,
|
id: record.id,
|
||||||
ehash: emailhash
|
ehash: emailhash,
|
||||||
|
}),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
} catch (err) {
|
||||||
})
|
|
||||||
}
|
|
||||||
catch(err) {
|
|
||||||
log.warn(err, 'Unable to create confirmation at signup')
|
log.warn(err, 'Unable to create confirmation at signup')
|
||||||
return res.status(500).send({ result: 'error', error: 'updateCreatedAccountUsernameFailed'})
|
return res.status(500).send({ result: 'error', error: 'updateCreatedAccountUsernameFailed' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send out signup email
|
// Send out signup email
|
||||||
|
@ -103,10 +128,9 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
req.body.to,
|
req.body.to,
|
||||||
...emailTemplate.signup(req.body.email, req.body.language, confirmation.id)
|
...emailTemplate.signup(req.body.email, req.body.language, confirmation.id)
|
||||||
)
|
)
|
||||||
}
|
} catch (err) {
|
||||||
catch(err) {
|
|
||||||
log.warn(err, 'Unable to send email')
|
log.warn(err, 'Unable to send email')
|
||||||
return res.status(500).send({ error: 'failedToSendSignupEmail', result})
|
return res.status(500).send({ error: 'failedToSendSignupEmail', result })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.body.unittest && req.body.email.split('@').pop() === 'mailtrap.freesewing.dev') {
|
if (req.body.unittest && req.body.email.split('@').pop() === 'mailtrap.freesewing.dev') {
|
||||||
|
@ -115,7 +139,7 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
result: 'success',
|
result: 'success',
|
||||||
status: 'created',
|
status: 'created',
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
confirmation: confirmation.id
|
confirmation: confirmation.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +148,6 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
: res.status(500).send({ error: 'unableToSendSignupEmail', result })
|
: res.status(500).send({ error: 'unableToSendSignupEmail', result })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Confirm account (after signup)
|
* Confirm account (after signup)
|
||||||
*
|
*
|
||||||
|
@ -133,50 +156,78 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
*/
|
*/
|
||||||
UserController.prototype.confirm = async (req, res, tools) => {
|
UserController.prototype.confirm = async (req, res, tools) => {
|
||||||
if (!req.params.id) return res.status(404).send({ error: 'missingConfirmationId', result })
|
if (!req.params.id) return res.status(404).send({ error: 'missingConfirmationId', result })
|
||||||
if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result})
|
if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result })
|
||||||
if (!req.body.consent || req.body.consent < 1) return res.status(400).send({ error: 'consentRequired', result })
|
if (!req.body.consent || req.body.consent < 1)
|
||||||
|
return res.status(400).send({ error: 'consentRequired', result })
|
||||||
|
|
||||||
// Destructure what we need from tools
|
// Destructure what we need from tools
|
||||||
const { prisma, config, encrypt } = tools
|
const { prisma, config, decrypt } = tools
|
||||||
|
|
||||||
// Retrieve confirmation record
|
// Retrieve confirmation record
|
||||||
let confirmation
|
let confirmation
|
||||||
try {
|
try {
|
||||||
confirmation = await prisma.confirmation.findUnique({
|
confirmation = await prisma.confirmation.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: req.params.id
|
id: req.params.id,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn(err, `Could not find confirmation id ${req.params.id}`)
|
log.warn(err, `Could not lookup confirmation id ${req.params.id}`)
|
||||||
return res.status(404).send({ error: 'failedToRetrieveConfirmationId', result})
|
return res.status(404).send({ error: 'failedToRetrieveConfirmationId', result })
|
||||||
}
|
}
|
||||||
console.log({confirmation, id: req.params})
|
if (!confirmation) {
|
||||||
return res.status(200).send({})
|
log.warn(err, `Could not find confirmation id ${req.params.id}`)
|
||||||
|
return res.status(404).send({ error: 'failedToFindConfirmationId', result })
|
||||||
|
}
|
||||||
|
if (confirmation.type !== 'signup') {
|
||||||
|
log.warn(err, `Confirmation mismatch; ${req.params.id} is not a signup id`)
|
||||||
|
return res.status(404).send({ error: 'confirmationIdTypeMismatch', result })
|
||||||
|
}
|
||||||
|
const data = decrypt(confirmation.data)
|
||||||
|
|
||||||
/*
|
// Retrieve user account
|
||||||
Confirmation.findById(req.body.id, (err, confirmation) => {
|
let account
|
||||||
if (err) return res.sendStatus(400)
|
try {
|
||||||
if (confirmation === null) return res.sendStatus(401)
|
account = await prisma.user.findUnique({
|
||||||
User.findOne({ handle: confirmation.data.handle }, (err, user) => {
|
where: {
|
||||||
if (err) return res.sendStatus(400)
|
id: data.id,
|
||||||
if (user === null) return res.sendStatus(401)
|
},
|
||||||
user.status = 'active'
|
|
||||||
user.consent = req.body.consent
|
|
||||||
user.time.login = new Date()
|
|
||||||
log.info('accountActivated', { handle: user.handle })
|
|
||||||
let account = user.account()
|
|
||||||
let token = getToken(account)
|
|
||||||
user.save(function (err) {
|
|
||||||
if (err) return res.sendStatus(400)
|
|
||||||
Confirmation.findByIdAndDelete(req.body.id, (err, confirmation) => {
|
|
||||||
return res.send({ account, people: {}, patterns: {}, token })
|
|
||||||
})
|
})
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(err, `Could not lookup user id ${data.id} from confirmation data`)
|
||||||
|
return res.status(404).send({ error: 'failedToRetrieveUserIdFromConfirmationData', result })
|
||||||
|
}
|
||||||
|
if (!account) {
|
||||||
|
log.warn(err, `Could not find user id ${data.id} from confirmation data`)
|
||||||
|
return res.status(404).send({ error: 'failedToLoadUserFromConfirmationData', result })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user consent and status
|
||||||
|
let updateUser
|
||||||
|
try {
|
||||||
|
updateUser = prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: data.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: 1,
|
||||||
|
consent: req.body.consent,
|
||||||
|
lastLogin: 'CURRENT_TIMESTAMP',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(err, `Could not update user id ${data.id} after confirmation`)
|
||||||
|
return res.status(404).send({ error: 'failedToUpdateUserAfterConfirmation', result })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account is now active, let's return a passwordless login
|
||||||
|
return res.status(200).send({
|
||||||
|
result: 'success',
|
||||||
|
token: getToken(account, config),
|
||||||
|
account: asAccount({ ...account, status: 1, consent: req.body.consent }, decrypt),
|
||||||
})
|
})
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
UserController.prototype.login = function (req, res, prisma, config) {
|
UserController.prototype.login = function (req, res, prisma, config) {
|
||||||
if (!req.body || !req.body.username) return res.sendStatus(400)
|
if (!req.body || !req.body.username) return res.sendStatus(400)
|
||||||
|
|
|
@ -61,7 +61,7 @@ describe('Non language-specific User controller signup routes', () => {
|
||||||
.post('/signup')
|
.post('/signup')
|
||||||
.send({
|
.send({
|
||||||
...data,
|
...data,
|
||||||
unittest: true
|
unittest: true,
|
||||||
})
|
})
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.status).to.equal(201)
|
expect(res.status).to.equal(201)
|
||||||
|
@ -69,28 +69,29 @@ describe('Non language-specific User controller signup routes', () => {
|
||||||
expect(res.charset).to.equal('utf-8')
|
expect(res.charset).to.equal('utf-8')
|
||||||
expect(res.body.result).to.equal(`success`)
|
expect(res.body.result).to.equal(`success`)
|
||||||
expect(res.body.email).to.equal(email)
|
expect(res.body.email).to.equal(email)
|
||||||
console.log(res.body)
|
|
||||||
store.confirmation = res.body.confirmation
|
store.confirmation = res.body.confirmation
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
step(`should confirm a new user (${email})`, (done) => {
|
step(`should confirm a new user (${email})`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.post(`/confirm/signup/${store.confirmation}`)
|
.post(`/confirm/signup/${store.confirmation}`)
|
||||||
.send({ consent: 1 })
|
.send({ consent: 1 })
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
console.log(res)
|
|
||||||
expect(res.status).to.equal(200)
|
expect(res.status).to.equal(200)
|
||||||
expect(res.type).to.equal('application/json')
|
expect(res.type).to.equal('application/json')
|
||||||
expect(res.charset).to.equal('utf-8')
|
expect(res.charset).to.equal('utf-8')
|
||||||
expect(res.body.result).to.equal(`success`)
|
expect(res.body.result).to.equal(`success`)
|
||||||
console.log(res)
|
expect(typeof res.body.token).to.equal(`string`)
|
||||||
|
expect(typeof res.body.account.id).to.equal(`number`)
|
||||||
|
store.token = res.body.token
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should fail to signup an existing email address', (done) => {
|
step('should fail to signup an existing email address', (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.post('/signup')
|
.post('/signup')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue