From f6a796959b7ab9390882795f8f0ea03ecb8681c0 Mon Sep 17 00:00:00 2001 From: joostdecock Date: Wed, 2 Nov 2022 12:33:32 +0100 Subject: [PATCH] wip(backend): Added lowercase username field --- .prettierignore | 1 + sites/backend/prisma/schema.prisma | 3 +- sites/backend/prisma/schema.sqlite | Bin 69632 -> 69632 bytes sites/backend/scripts/json-to-sqlite.mjs | 49 ++++++++----- sites/backend/src/controllers/user.mjs | 89 ++++++++++++----------- sites/backend/src/routes/user.mjs | 20 ++--- sites/backend/tests/user.test.mjs | 51 +++++++++---- 7 files changed, 122 insertions(+), 91 deletions(-) diff --git a/.prettierignore b/.prettierignore index abf3eaf81c4..6ded7ed6886 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ *.png *.svg *.prisma +*.sqlite yarn.lock .husky/pre-commit .prettierignore diff --git a/sites/backend/prisma/schema.prisma b/sites/backend/prisma/schema.prisma index 3f16b2f6449..000c42019aa 100644 --- a/sites/backend/prisma/schema.prisma +++ b/sites/backend/prisma/schema.prisma @@ -41,7 +41,8 @@ model User { role String @default("user") status Int @default(0) updatedAt DateTime? @updatedAt - username String @unique + username String + lusername String @unique @@index([ihash]) } diff --git a/sites/backend/prisma/schema.sqlite b/sites/backend/prisma/schema.sqlite index 8fb4d4ab5d75d4160974121860cadf9e251c9c64..abc9f57a94a7ff7c42cce511daef775a944354fe 100644 GIT binary patch delta 382 zcmZozz|ydQWrDPzHUk5LAOiyfGXnzy69WT-`a~ULU2O(E{~BJFqYQkE7Z|uacvrEW zWvge&;;`V_$i~mF%Ko460&5wo5N{OY5f;tKf&$7L*NZZ$upDJt#&VQtk+`fZW2?9< zySS(*W0zn_Vp2|OXmM&02s3SV=MG?G^w~UvM}Tp1J`X!%&g6-_l9RJoqFLCO7_z$m+!lWigk6Sw~4Ib4%j`1UbrZ#L#u zXJYbGn0%Z?mXUk%Qx=2Gt9TQIczGBY7`Pc27`PZ17}z!o3cTRoWWZ^_{)Ybn|9Ae& xn*{|9^Yik5XW-??OD&HFxnuH!dF3hhifyV!GHZt4lD|rSq}X12LK_>Y7zhd delta 430 zcmZozz|ydQWrDPz3IhX!AOiyfGXnzy69WT-;zS)|T@?mB{~BI4CI&v1ECwzQ-c_t; zx%FAgSVdWmvemO>aaeF|WaH;oW&h8j$)Y(~P(XR(dQry7``KzXf9GIgRAyshcrLE1 z%h*(1l9-f}nwMH0@0_2PmRXdWSdy8a2jWk5;Fo3rGotyII ({ status: user.status, updatedAt: user.updatedAt, username: user.username, + lusername: user.lusername, }) const getToken = (user, config) => @@ -72,6 +73,7 @@ UserController.prototype.signup = async (req, res, tools) => { // It does not. Creating user entry let record try { + const username = clean(randomString()) // Temporary username, record = await prisma.user.create({ data: { data: asJson({ settings: { language: req.body.language } }), @@ -80,7 +82,8 @@ UserController.prototype.signup = async (req, res, tools) => { ihash: emailhash, initial: encrypt(clean(req.body.email)), password: asJson(hashPassword(req.body.password)), - username: randomString(), // Temporary username, + username, + lusername: username.toLowerCase(), }, }) } catch (err) { @@ -94,6 +97,7 @@ UserController.prototype.signup = async (req, res, tools) => { where: { id: record.id }, data: { username: `user-${record.id}`, + lusername: `user-${record.id}`, }, }) } catch (err) { @@ -229,47 +233,50 @@ UserController.prototype.confirm = async (req, res, tools) => { } /* -UserController.prototype.login = function (req, res, prisma, config) { - if (!req.body || !req.body.username) return res.sendStatus(400) - User.findOne( - { - $or: [ - { username: req.body.username.toLowerCase().trim() }, - { username: req.body.username.trim() }, - { ehash: ehash(req.body.username) }, - ], - }, - (err, user) => { - if (err) return res.sendStatus(400) - if (user === null) return res.sendStatus(401) - user.verifyPassword(req.body.password, (err, valid) => { - if (err) return res.sendStatus(400) - else if (valid) { - if (user.status !== 'active') return res.sendStatus(403) - else { - log.info('login', { user, req }) - let account = user.account() - let token = getToken(account) - let people = {} - Person.find({ user: user.handle }, (err, personList) => { - if (err) return res.sendStatus(400) - for (let person of personList) people[person.handle] = person.info() - let patterns = {} - Pattern.find({ user: user.handle }, (err, patternList) => { - if (err) return res.sendStatus(400) - for (let pattern of patternList) patterns[pattern.handle] = pattern - return user.updateLoginTime(() => res.send({ account, people, patterns, token })) - }) - }) - } - } else { - log.warning('wrongPassword', { user, req }) - return res.sendStatus(401) - } - }) - } - ) + * Login (with username and password) + * + * This is the endpoint that provides traditional username/password login + * 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 }) + + // 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: 'logingFailed', result }) + } + if (!account) { + log.warn(`Login attempt for non-existing user: ${req.body.username} from ${req.ip}`) + return res.status(401).send({ error: 'logingFailed', result }) + } + + // Login success + log.info(`Login by user ${account.id} (${account.username})`) + + return res.status(200).send({ + result: 'success', + token: getToken(account, config), + account: asAccount({ ...account }, decrypt), + }) } +/* // For people who have forgotten their password, or password-less logins UserController.prototype.confirmationLogin = function (req, res) { diff --git a/sites/backend/src/routes/user.mjs b/sites/backend/src/routes/user.mjs index 91710eb5b95..9886f1b4f1d 100644 --- a/sites/backend/src/routes/user.mjs +++ b/sites/backend/src/routes/user.mjs @@ -3,20 +3,16 @@ import { UserController } from '../controllers/user.mjs' const User = new UserController() const jwt = ['jwt', { session: false }] - export function userRoutes(tools) { const { app, passport } = tools // Sign up - app.post( - '/signup', - (req, res) => User.signup(req, res, tools) - ) + app.post('/signup', (req, res) => User.signup(req, res, tools)) // Confirm account - app.post( - '/confirm/signup/:id', - (req, res) => User.confirm(req, res, tools) - ) + app.post('/confirm/signup/:id', (req, res) => User.confirm(req, res, tools)) + + // Login + app.post('/login', (req, res) => User.login(req, res, tools)) // Create account //app.post( @@ -79,11 +75,6 @@ export function userRoutes(tools) { (req, res) => User.resend(req, res, params) ) - // Login - app.post( - '/login', - (req, res) => User.login(req, res, params) - ) // Get list of patrons app.get( @@ -105,4 +96,3 @@ export function userRoutes(tools) { ) */ } - diff --git a/sites/backend/tests/user.test.mjs b/sites/backend/tests/user.test.mjs index 1d11f04a4f9..db629f5f205 100644 --- a/sites/backend/tests/user.test.mjs +++ b/sites/backend/tests/user.test.mjs @@ -6,12 +6,17 @@ import { randomString } from '../src/utils/crypto.mjs' const config = verifyConfig() const expect = chai.expect chai.use(http) +const user = '🧑' -const email = `test_${randomString()}@mailtrap.freesewing.dev` const store = {} +const data = { + email: `test_${randomString()}@mailtrap.freesewing.dev`, + language: 'en', + password: 'One two one two, this is just a test', +} -describe('Non language-specific User controller signup routes', () => { - it('Should return 400 on signup without body', (done) => { +describe(`${user} Signup flow and authentication`, () => { + it(`${user} Should return 400 on signup without body`, (done) => { chai .request(config.api) .post('/signup') @@ -25,14 +30,8 @@ describe('Non language-specific User controller signup routes', () => { }) }) - const data = { - email, - language: 'en', - password: 'One two one two, this is just a test', - } - Object.keys(data).map((key) => { - it(`Should not create signup without ${key}`, (done) => { + it(`${user} Should not allow signup without ${key}`, (done) => { chai .request(config.api) .post('/signup') @@ -55,7 +54,7 @@ describe('Non language-specific User controller signup routes', () => { }) }) - step(`should signup a new user (${email})`, (done) => { + step(`${user} Should signup new user ${data.email}`, (done) => { chai .request(config.api) .post('/signup') @@ -68,13 +67,13 @@ describe('Non language-specific User controller signup routes', () => { expect(res.type).to.equal('application/json') expect(res.charset).to.equal('utf-8') expect(res.body.result).to.equal(`success`) - expect(res.body.email).to.equal(email) + expect(res.body.email).to.equal(data.email) store.confirmation = res.body.confirmation done() }) }) - step(`should confirm a new user (${email})`, (done) => { + step(`${user} Should confirm new user (${data.email})`, (done) => { chai .request(config.api) .post(`/confirm/signup/${store.confirmation}`) @@ -87,11 +86,12 @@ describe('Non language-specific User controller signup routes', () => { expect(typeof res.body.token).to.equal(`string`) expect(typeof res.body.account.id).to.equal(`number`) store.token = res.body.token + store.username = res.body.account.username done() }) }) - step('should fail to signup an existing email address', (done) => { + step(`${user} Should fail to signup an existing email address`, (done) => { chai .request(config.api) .post('/signup') @@ -106,6 +106,29 @@ describe('Non language-specific User controller signup routes', () => { }) }) + step(`${user} Should login with username and password`, (done) => { + chai + .request(config.api) + .post('/login') + .send({ + username: store.username, + password: data.email, + }) + .end((err, res) => { + expect(res.status).to.equal(200) + expect(res.type).to.equal('application/json') + expect(res.charset).to.equal('utf-8') + expect(res.body.result).to.equal(`success`) + expect(res.body.account.email).to.equal(data.email) + expect(res.body.account.username).to.equal(store.username) + expect(res.body.account.lusername).to.equal(store.username.toLowerCase()) + expect(typeof res.body.token).to.equal(`string`) + expect(typeof res.body.account.id).to.equal(`number`) + store.token = res.body.token + done() + }) + }) + /*