diff --git a/sites/backend/scripts/find-duplicate-usernames.mjs b/sites/backend/scripts/find-duplicate-usernames.mjs new file mode 100644 index 00000000000..a0744da8046 --- /dev/null +++ b/sites/backend/scripts/find-duplicate-usernames.mjs @@ -0,0 +1,51 @@ +import path from 'path' +import fs from 'fs' +import chalk from 'chalk' + +/* + * Note: This is not intended to work for you + * + * This script imports a raw database dump of the current (v2) + * FreeSewing backend and checks for duplicate usernames now that + * we treat them as case-insensitive. + * + * This is not the kind of thing you should try to run yourself + * because for one thing you do not have a raw database dump + */ + +// Dumped data folder +const dir = '/home/joost/' +let i = 0 + +// Load filtered data for migration +const file = 'freesewing-filtered.json' +const data = JSON.parse(fs.readFileSync(path.resolve(dir, file), { encoding: 'utf-8' })) +console.log() +console.log('Checking:') +console.log(' 🧑 ', Object.keys(data.users).length, 'users') +console.log() +data.lusernames = {} +await checkUsers(data.users) +console.log() + +async function checkUsers(users) { + const total = Object.keys(users).length + let i = 0 + for (const user of Object.values(users)) { + i++ + await checkUser(user) + } +} + +async function checkUser(user) { + const lusername = user.username.toLowerCase() + if (typeof data.lusernames[lusername] === 'undefined') { + data.lusernames[lusername] = user + } else { + i++ + const first = data.lusernames[lusername] + console.log(chalk.yellow(`${i}: ${lusername}`)) + console.log(` - First by: ${chalk.green(first.handle)} / ${chalk.green(first.email)}`) + console.log(` - Later by: ${chalk.cyan(user.handle)} / ${chalk.cyan(user.email)}`) + } +} diff --git a/sites/backend/scripts/json-to-sqlite.mjs b/sites/backend/scripts/json-to-sqlite.mjs index 09583011efa..9a98d2c59a0 100644 --- a/sites/backend/scripts/json-to-sqlite.mjs +++ b/sites/backend/scripts/json-to-sqlite.mjs @@ -32,7 +32,6 @@ console.log(' 🕺 ', Object.keys(data.people).length, 'people') console.log(' 👕 ', Object.keys(data.patterns).length, 'patterns') console.log(' 📰 ', data.subscribers.length, 'subscribers') console.log() -data.lusernames = {} data.userhandles = {} await migrateUsers(data.users) console.log() @@ -127,34 +126,39 @@ async function migrateUsers(users) { async function createUser(user) { const ehash = hash(clean(user.email)) let record + const _data = { + consent: user.consent, + createdAt: user.createdAt, + data: JSON.stringify(user.data), + ehash, + email: encrypt(clean(user.email)), + ihash: ehash, + initial: encrypt(clean(user.email)), + newsletter: user.newsletter, + password: JSON.stringify({ + type: 'v2', + data: user.password, + }), + patron: user.patron, + role: user.role, + status: user.status, + username: user.username, + lusername: user.username.toLowerCase(), + lastLogin: new Date(user.lastLogin), + } try { - record = await prisma.user.create({ - data: { - consent: user.consent, - createdAt: user.createdAt, - data: JSON.stringify(user.data), - ehash, - email: encrypt(clean(user.email)), - ihash: ehash, - initial: encrypt(clean(user.email)), - newsletter: user.newsletter, - password: JSON.stringify({ - type: 'v2', - data: user.password, - }), - patron: user.patron, - role: user.role, - status: user.status, - username: user.username, - lusername: user.username.toLowerCase(), - }, - }) + record = await prisma.user.create({ data: _data }) } catch (err) { - console.log(user, err, data.lusernames[user.username.toLowerCase()]) - process.exit() + _data.username += ' 2' + _data.lusername += ' 2' + try { + record = await prisma.user.create({ data: _data }) + } catch (err) { + console.log(err) + process.exit() + } } data.userhandles[user.handle] = record.id - data.lusernames[record.lusername] = user } /* diff --git a/sites/backend/scripts/mongo-to-json.mjs b/sites/backend/scripts/mongo-to-json.mjs index 7aef9d23b6c..f7d139618f3 100644 --- a/sites/backend/scripts/mongo-to-json.mjs +++ b/sites/backend/scripts/mongo-to-json.mjs @@ -116,6 +116,7 @@ function migrateUser(entry) { initial: entry.initial, newsletter: entry.newsletter, img: entry.img, + lastLogin, } } diff --git a/sites/backend/src/controllers/user.mjs b/sites/backend/src/controllers/user.mjs index e1fbb6c3976..d782ad9ca60 100644 --- a/sites/backend/src/controllers/user.mjs +++ b/sites/backend/src/controllers/user.mjs @@ -209,14 +209,14 @@ UserController.prototype.confirm = async (req, res, tools) => { // Update user consent and status let updateUser try { - updateUser = prisma.user.update({ + updateUser = await prisma.user.update({ where: { - id: data.id, + id: account.id, }, data: { status: 1, consent: req.body.consent, - lastLogin: 'CURRENT_TIMESTAMP', + lastLogin: new Date(), }, }) } catch (err) { @@ -302,6 +302,37 @@ UserController.prototype.login = async function (req, res, tools) { account: asAccount({ ...account }, decrypt), }) } + +UserController.prototype.readAccount = async (req, res, tools) => { + if (!req.user?._id) return res.status(400).send({ error: 'bearerMissing', result }) + + // Destructure what we need from tools + const { prisma, decrypt } = tools + + // Retrieve user account + let account + try { + account = await prisma.user.findUnique({ + where: { + id: req.user._id, + }, + }) + } catch (err) { + log.warn(err, `Could not lookup user id ${req.user._id} from token data`) + return res.status(404).send({ error: 'failedToRetrieveUserIdFromTokenData', result }) + } + if (!account) { + log.warn(err, `Could not find user id ${req.user._id} from token data`) + return res.status(404).send({ error: 'failedToLoadUserFromTokenData', result }) + } + + // Return account data + return res.status(200).send({ + result: 'success', + account: asAccount({ ...account }, decrypt), + }) +} + /* // For people who have forgotten their password, or password-less logins @@ -365,28 +396,6 @@ UserController.prototype.create = (req, res) => { }) } -UserController.prototype.readAccount = (req, res) => { - if (!req.user._id) return res.sendStatus(400) - User.findById(req.user._id, (err, user) => { - if (user !== null) { - log.info('ping', { user, req }) - const people = {} - Person.find({ user: user.handle }, (err, personList) => { - if (err) return res.sendStatus(400) - for (let person of personList) people[person.handle] = person.info() - const patterns = {} - Pattern.find({ user: user.handle }, (err, patternList) => { - if (err) return res.sendStatus(400) - for (let pattern of patternList) patterns[pattern.handle] = pattern.export() - return res.send({ account: user.account(), people, patterns }) - }) - }) - } else { - return res.sendStatus(400) - } - }) -} - UserController.prototype.readProfile = (req, res) => { User.findOne({ username: req.params.username }, (err, user) => { if (err) return res.sendStatus(404) diff --git a/sites/backend/src/routes/user.mjs b/sites/backend/src/routes/user.mjs index 9886f1b4f1d..4dba8e8d3ab 100644 --- a/sites/backend/src/routes/user.mjs +++ b/sites/backend/src/routes/user.mjs @@ -14,6 +14,11 @@ export function userRoutes(tools) { // Login app.post('/login', (req, res) => User.login(req, res, tools)) + // Read account (own data) + app.get('/account', passport.authenticate(...jwt), (req, res) => + User.readAccount(req, res, tools) + ) + // Create account //app.post( // '/account', @@ -21,12 +26,6 @@ export function userRoutes(tools) { //) /* - // Read account (own data) - app.get( - '/account', - passport.authenticate(...jwt), - (req, res) => User.readAccount(req, res, params) - ) // Update account app.put( diff --git a/sites/backend/tests/user.test.mjs b/sites/backend/tests/user.test.mjs index 50cf5e096f5..c225048258d 100644 --- a/sites/backend/tests/user.test.mjs +++ b/sites/backend/tests/user.test.mjs @@ -240,15 +240,11 @@ describe(`${user} Signup flow and authentication`, () => { }) }) - /* - step(`${user} Should login with email address and password`, (done) => { + step(`${user} Should load account with JWT`, (done) => { chai .request(config.api) - .post('/login') - .send({ - username: store.username, - password: data.email, - }) + .get('/account') + .set('Authorization', 'Bearer ' + store.token) .end((err, res) => { expect(res.status).to.equal(200) expect(res.type).to.equal('application/json') @@ -257,52 +253,14 @@ describe(`${user} Signup flow and authentication`, () => { 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() }) }) + /* -describe('Login/Logout and session handling', () => { - - it('should login with the email address', (done) => { - chai - .request(config.backend) - .post('/login') - .send({ - username: config.user.email, - password: config.user.password, - }) - .end((err, res) => { - res.should.have.status(200) - let data = JSON.parse(res.text) - data.account.username.should.equal(config.user.username) - data.token.should.be.a('string') - done() - }) - }) - - it('should load account with JSON Web Token', (done) => { - chai - .request(config.backend) - .get('/account') - .set('Authorization', 'Bearer ' + config.user.token) - .end((err, res) => { - if (err) console.log(err) - let data = JSON.parse(res.text) - res.should.have.status(200) - data.account.username.should.equal(config.user.username) - // Enable this once cleanup is implemented - //Object.keys(data.recipes).length.should.equal(0) - //Object.keys(data.people).length.should.equal(0) - done() - }) - }) -}) - describe('Account management', () => { it('should update the account avatar', (done) => { chai