1
0
Fork 0

wip(backend): Singup workflow and tests

This commit is contained in:
joostdecock 2022-11-01 21:36:15 +01:00
parent 0eaccf7b18
commit efe9e6c24d
5 changed files with 113 additions and 60 deletions

View file

@ -6,6 +6,7 @@
*.mustache *.mustache
*.png *.png
*.svg *.svg
*.prisma
yarn.lock yarn.lock
.husky/pre-commit .husky/pre-commit
.prettierignore .prettierignore

View file

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

View file

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

View file

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

View file

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