wip(backend): moved more logic to user model
This commit is contained in:
parent
487095eecb
commit
819656815c
4 changed files with 136 additions and 75 deletions
|
@ -92,68 +92,10 @@ UserController.prototype.confirm = async (req, res, tools) => {
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
UserController.prototype.login = async function (req, res, tools) {
|
UserController.prototype.login = async function (req, res, tools) {
|
||||||
if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result })
|
const User = new UserModel(tools)
|
||||||
if (!req.body.username) return res.status(400).json({ error: 'usernameMissing', result })
|
await User.passwordLogin(req)
|
||||||
if (!req.body.password) return res.status(400).json({ error: 'passwordMissing', result })
|
|
||||||
|
|
||||||
// Destructure what we need from tools
|
return User.sendResponse(res)
|
||||||
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),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UserController.prototype.whoami = async (req, res, tools) => {
|
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) => {
|
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
|
// Destructure what we need from tools
|
||||||
const { prisma, decrypt } = tools
|
const { prisma, decrypt } = tools
|
||||||
|
|
|
@ -21,13 +21,44 @@ export function UserModel(tools) {
|
||||||
* Stores result in this.record
|
* Stores result in this.record
|
||||||
*/
|
*/
|
||||||
UserModel.prototype.read = async function (where) {
|
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?.email) this.email = this.decrypt(this.record.email)
|
||||||
if (this.record?.initial) this.initial = this.decrypt(this.record.initial)
|
if (this.record?.initial) this.initial = this.decrypt(this.record.initial)
|
||||||
|
|
||||||
return this.setExists()
|
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
|
* Loads the user that is making the API request
|
||||||
*
|
*
|
||||||
|
@ -84,7 +115,7 @@ UserModel.prototype.create = async function ({ body }) {
|
||||||
initial: email,
|
initial: email,
|
||||||
username,
|
username,
|
||||||
lusername: username,
|
lusername: username,
|
||||||
data: asJson({ settings: { language: this.language } }),
|
data: this.encrypt(asJson({ settings: { language: this.language } })),
|
||||||
password: asJson(hashPassword(body.password)),
|
password: asJson(hashPassword(body.password)),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -134,6 +165,38 @@ UserModel.prototype.create = async function ({ body }) {
|
||||||
: this.setResponse(201, false, { email: this.email })
|
: 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
|
* Confirms a user account
|
||||||
*/
|
*/
|
||||||
|
@ -169,18 +232,14 @@ UserModel.prototype.confirm = async function ({ body, params }) {
|
||||||
|
|
||||||
// Update user status, consent, and last login
|
// Update user status, consent, and last login
|
||||||
await this.update({
|
await this.update({
|
||||||
//data: this.encrypt({...this.decrypt(this.record.data), status: 1}),
|
status: 1,
|
||||||
consent: body.consent,
|
consent: body.consent,
|
||||||
lastLogin: new Date(),
|
lastLogin: new Date(),
|
||||||
})
|
})
|
||||||
if (this.error) return this
|
if (this.error) return this
|
||||||
|
|
||||||
// Account is now active, let's return a passwordless login
|
// Account is now active, let's return a passwordless login
|
||||||
return this.setResponse(200, false, {
|
return this.loginOk()
|
||||||
result: 'success',
|
|
||||||
token: this.getToken(),
|
|
||||||
account: this.asAccount(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -209,9 +268,9 @@ UserModel.prototype.asAccount = function () {
|
||||||
id: this.record.id,
|
id: this.record.id,
|
||||||
consent: this.record.consent,
|
consent: this.record.consent,
|
||||||
createdAt: this.record.createdAt,
|
createdAt: this.record.createdAt,
|
||||||
data: this.record.data,
|
data: JSON.parse(this.decrypt(this.record.data)),
|
||||||
email: this.email,
|
email: this.decrypt(this.record.email),
|
||||||
initial: this.initial,
|
initial: this.decrypt(this.record.initial),
|
||||||
lastLogin: this.record.lastLogin,
|
lastLogin: this.record.lastLogin,
|
||||||
newsletter: this.record.newsletter,
|
newsletter: this.record.newsletter,
|
||||||
patron: this.record.patron,
|
patron: this.record.patron,
|
||||||
|
@ -277,3 +336,31 @@ UserModel.prototype.sendResponse = async function (res) {
|
||||||
UserModel.prototype.isUnitTest = function (body) {
|
UserModel.prototype.isUnitTest = function (body) {
|
||||||
return body.unittest && this.email.split('@').pop() === this.config.tests.domain
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
30
sites/backend/tests/account.mjs
Normal file
30
sites/backend/tests/account.mjs
Normal file
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import http from 'chai-http'
|
||||||
import { verifyConfig } from '../src/config.mjs'
|
import { verifyConfig } from '../src/config.mjs'
|
||||||
import { randomString } from '../src/utils/crypto.mjs'
|
import { randomString } from '../src/utils/crypto.mjs'
|
||||||
import { userTests } from './user.mjs'
|
import { userTests } from './user.mjs'
|
||||||
|
import { accountTests } from './account.mjs'
|
||||||
import { apikeyTests } from './apikey.mjs'
|
import { apikeyTests } from './apikey.mjs'
|
||||||
import { setup } from './shared.mjs'
|
import { setup } from './shared.mjs'
|
||||||
|
|
||||||
|
@ -31,8 +32,9 @@ store.icon = (icon1, icon2 = false) => store.icons[icon1] + (icon2 ? store.icons
|
||||||
// Run tests
|
// Run tests
|
||||||
const runTests = async (config, store, chai) => {
|
const runTests = async (config, store, chai) => {
|
||||||
await setup(config, store, chai)
|
await setup(config, store, chai)
|
||||||
//await userTests(config, store, chai)
|
await userTests(config, store, chai)
|
||||||
await apikeyTests(config, store, chai)
|
//await apikeyTests(config, store, chai)
|
||||||
|
//await accountTests(config, store, chai)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do the work
|
// Do the work
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue