From 819656815cf6cd538ce55a9c1343cc902d6f43cf Mon Sep 17 00:00:00 2001 From: joostdecock Date: Mon, 7 Nov 2022 20:42:07 +0100 Subject: [PATCH] wip(backend): moved more logic to user model --- sites/backend/src/controllers/user.mjs | 66 +-------------- sites/backend/src/models/user.mjs | 109 ++++++++++++++++++++++--- sites/backend/tests/account.mjs | 30 +++++++ sites/backend/tests/index.mjs | 6 +- 4 files changed, 136 insertions(+), 75 deletions(-) create mode 100644 sites/backend/tests/account.mjs diff --git a/sites/backend/src/controllers/user.mjs b/sites/backend/src/controllers/user.mjs index b3a57fdcd94..e7c81c14fad 100644 --- a/sites/backend/src/controllers/user.mjs +++ b/sites/backend/src/controllers/user.mjs @@ -92,68 +92,10 @@ UserController.prototype.confirm = async (req, res, tools) => { * See: https://freesewing.dev/reference/backend/api */ UserController.prototype.login = async function (req, res, tools) { - if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result }) - if (!req.body.username) return res.status(400).json({ error: 'usernameMissing', result }) - if (!req.body.password) return res.status(400).json({ error: 'passwordMissing', result }) + const User = new UserModel(tools) + await User.passwordLogin(req) - // Destructure what we need from tools - const { prisma, config, decrypt } = tools - - // Retrieve user account - let account - try { - account = await prisma.user.findFirst({ - where: { - OR: [ - { lusername: { equals: clean(req.body.username) } }, - { ehash: { equals: hash(clean(req.body.username)) } }, - { id: { equals: parseInt(req.body.username) || -1 } }, - ], - }, - }) - } catch (err) { - log.warn(err, `Error while trying to find username: ${req.body.username}`) - return res.status(401).send({ error: 'loginFailed', result }) - } - if (!account) { - log.warn(`Login attempt for non-existing user: ${req.body.username} from ${req.ip}`) - return res.status(401).send({ error: 'loginFailed', result }) - } - - // Account found, check password - const [valid, updatedPasswordField] = verifyPassword(req.body.password, account.password) - if (!valid) { - log.warn(`Wrong password for existing user: ${req.body.username} from ${req.ip}`) - return res.status(401).send({ error: 'loginFailed', result }) - } - - // Login success - log.info(`Login by user ${account.id} (${account.username})`) - if (updatedPasswordField) { - // Update the password field with a v3 hash - let updateUser - try { - updateUser = await prisma.user.update({ - where: { - id: account.id, - }, - data: { - password: updatedPasswordField, - }, - }) - } catch (err) { - log.warn( - err, - `Could not update password field with v3 hash for user id ${account.id} (${account.username})` - ) - } - } - - return res.status(200).send({ - result: 'success', - token: getToken(account, config), - account: asAccount({ ...account }, decrypt), - }) + return User.sendResponse(res) } UserController.prototype.whoami = async (req, res, tools) => { @@ -187,7 +129,7 @@ UserController.prototype.whoami = async (req, res, tools) => { } UserController.prototype.update = async (req, res, tools) => { - if (!req.user?._id) return res.status(400).send({ error: 'bearerMissing', result }) + console.log('update please') // Destructure what we need from tools const { prisma, decrypt } = tools diff --git a/sites/backend/src/models/user.mjs b/sites/backend/src/models/user.mjs index 47e4f9d0732..59fb86a9971 100644 --- a/sites/backend/src/models/user.mjs +++ b/sites/backend/src/models/user.mjs @@ -21,13 +21,44 @@ export function UserModel(tools) { * Stores result in this.record */ UserModel.prototype.read = async function (where) { - this.record = await this.prisma.user.findUnique({ where }) + try { + this.record = await this.prisma.user.findUnique({ where }) + } catch (err) { + log.warn({ err, where }, 'Could not read user') + } + if (this.record?.email) this.email = this.decrypt(this.record.email) if (this.record?.initial) this.initial = this.decrypt(this.record.initial) return this.setExists() } +/* + * Finds a user based on one of the accepted unique fields which are: + * - lusername (lowercase username) + * - ehash + * - id + * + * Stores result in this.record + */ +UserModel.prototype.find = async function (body) { + try { + this.record = await this.prisma.user.findFirst({ + where: { + OR: [ + { lusername: { equals: clean(body.username) } }, + { ehash: { equals: hash(clean(body.username)) } }, + { id: { equals: parseInt(body.username) || -1 } }, + ], + }, + }) + } catch (err) { + log.warn({ err, body }, `Error while trying to find user: ${body.username}`) + } + + return this.setExists() +} + /* * Loads the user that is making the API request * @@ -84,7 +115,7 @@ UserModel.prototype.create = async function ({ body }) { initial: email, username, lusername: username, - data: asJson({ settings: { language: this.language } }), + data: this.encrypt(asJson({ settings: { language: this.language } })), password: asJson(hashPassword(body.password)), }, }) @@ -134,6 +165,38 @@ UserModel.prototype.create = async function ({ body }) { : this.setResponse(201, false, { email: this.email }) } +/* + * Login based on username + password + */ +UserModel.prototype.passwordLogin = async function (req) { + if (Object.keys(req.body) < 1) return this.setReponse(400, 'postBodyMissing') + if (!req.body.username) return this.setReponse(400, 'usernameMissing') + if (!req.body.password) return this.setReponse(400, 'passwordMissing') + + await this.find(req.body) + if (!this.exists) { + log.warn(`Login attempt for non-existing user: ${req.body.username} from ${req.ip}`) + return this.setResponse(401, 'loginFailed') + } + + // Account found, check password + const [valid, updatedPasswordField] = verifyPassword(req.body.password, this.record.password) + if (!valid) { + log.warn(`Wrong password for existing user: ${req.body.username} from ${req.ip}`) + return this.setResponse(401, 'loginFailed') + } + + log.info(`Login by user ${this.record.id} (${this.record.username})`) + + // Login success + if (updatedPasswordField) { + // Update the password field with a v3 hash + await this.update({ password: updatedPasswordField }) + } + + return this.isOk() ? this.loginOk() : this.setResponse(401, 'loginFailed') +} + /* * Confirms a user account */ @@ -169,18 +232,14 @@ UserModel.prototype.confirm = async function ({ body, params }) { // Update user status, consent, and last login await this.update({ - //data: this.encrypt({...this.decrypt(this.record.data), status: 1}), + status: 1, consent: body.consent, lastLogin: new Date(), }) if (this.error) return this // Account is now active, let's return a passwordless login - return this.setResponse(200, false, { - result: 'success', - token: this.getToken(), - account: this.asAccount(), - }) + return this.loginOk() } /* @@ -209,9 +268,9 @@ UserModel.prototype.asAccount = function () { id: this.record.id, consent: this.record.consent, createdAt: this.record.createdAt, - data: this.record.data, - email: this.email, - initial: this.initial, + data: JSON.parse(this.decrypt(this.record.data)), + email: this.decrypt(this.record.email), + initial: this.decrypt(this.record.initial), lastLogin: this.record.lastLogin, newsletter: this.record.newsletter, patron: this.record.patron, @@ -277,3 +336,31 @@ UserModel.prototype.sendResponse = async function (res) { UserModel.prototype.isUnitTest = function (body) { return body.unittest && this.email.split('@').pop() === this.config.tests.domain } + +/* + * Helper method to check an account is ok + */ +UserModel.prototype.isOk = function () { + if ( + this.exists && + this.record && + this.record.status > 0 && + this.record.consent > 0 && + this.record.role && + this.record.role !== 'blocked' + ) + return true + + return false +} + +/* + * Helper method to return from successful login + */ +UserModel.prototype.loginOk = function () { + return this.setResponse(200, false, { + result: 'success', + token: this.getToken(), + account: this.asAccount(), + }) +} diff --git a/sites/backend/tests/account.mjs b/sites/backend/tests/account.mjs new file mode 100644 index 00000000000..5daf9532288 --- /dev/null +++ b/sites/backend/tests/account.mjs @@ -0,0 +1,30 @@ +export const accountTests = async (config, store, chai) => { + const expect = chai.expect + + /* + consent Int @default(0) + data String @default("{}") + ehash String @unique + email String + newsletter Boolean @default(false) + password String + username String + lusername String @unique + */ + + describe(`${store.icon('user')} Update account data`, async function () { + it(`${store.icon('user')} Should update consent to 3 (jwt)`, (done) => { + chai + .request(config.api) + .put('/account/jwt') + .set('Authorization', 'Bearer ' + store.account.token) + .send({ consent: 3 }) + .end((err, res) => { + expect(err === null).to.equal(true) + expect(res.status).to.equal(200) + expect(res.body.result).to.equal(`success`) + done() + }) + }) + }) +} diff --git a/sites/backend/tests/index.mjs b/sites/backend/tests/index.mjs index a7d21a15ab8..cf4ede6569d 100644 --- a/sites/backend/tests/index.mjs +++ b/sites/backend/tests/index.mjs @@ -4,6 +4,7 @@ import http from 'chai-http' import { verifyConfig } from '../src/config.mjs' import { randomString } from '../src/utils/crypto.mjs' import { userTests } from './user.mjs' +import { accountTests } from './account.mjs' import { apikeyTests } from './apikey.mjs' import { setup } from './shared.mjs' @@ -31,8 +32,9 @@ store.icon = (icon1, icon2 = false) => store.icons[icon1] + (icon2 ? store.icons // Run tests const runTests = async (config, store, chai) => { await setup(config, store, chai) - //await userTests(config, store, chai) - await apikeyTests(config, store, chai) + await userTests(config, store, chai) + //await apikeyTests(config, store, chai) + //await accountTests(config, store, chai) } // Do the work