Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial seeding support using users #285

Merged
merged 16 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ POSTGRES_HOST=db
POSTGRES_PORT=5432
POSTGRES_DB=wabs
POSTGRES_DB_TEST=wabs_system_test
DEFAULT_USER_PASSWORD=P@55word

# Server config
PORT=3000
Expand Down
32 changes: 22 additions & 10 deletions app/controllers/data/data.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,40 @@
const Boom = require('@hapi/boom')

const DbExportService = require('../../services/db-export/db-export.service.js')
const SeedService = require('../../services/data/seed/seed.service.js')
const TearDownService = require('../../services/data/tear-down/tear-down.service.js')

async function tearDown (_request, h) {
/**
* Triggers export of all relevant tables to CSV and then uploads them to S3
*/
async function dbExport (_request, h) {
DbExportService.go()

return h.response().code(204)
}

async function seed (_request, h) {
try {
await TearDownService.go()
await SeedService.go()

return h.response().code(204)
} catch (error) {
return Boom.badImplementation(error.message)
}
}

/**
* Triggers export of all relevant tables to CSV and then uploads them to S3
*/
async function dbExport (_request, h) {
DbExportService.go()
async function tearDown (_request, h) {
try {
await TearDownService.go()

return h.response().code(204)
return h.response().code(204)
} catch (error) {
return Boom.badImplementation(error.message)
}
}

module.exports = {
tearDown,
dbExport
dbExport,
seed,
tearDown
}
23 changes: 16 additions & 7 deletions app/routes/data.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,30 @@
const DataController = require('../controllers/data/data.controller.js')

const routes = [
{
method: 'GET',
path: '/data/db-export',
handler: DataController.dbExport,
options: {
description: 'Used to export the database and upload the file to our AWS S3 bucket',
app: { excludeFromProd: true }
}
},
{
method: 'POST',
path: '/data/tear-down',
handler: DataController.tearDown,
path: '/data/seed',
handler: DataController.seed,
options: {
description: 'Used to remove the acceptance test data from the database',
description: 'Used to seed test data in the database',
app: { excludeFromProd: true }
}
},
{
method: 'GET',
path: '/data/db-export',
handler: DataController.dbExport,
method: 'POST',
path: '/data/tear-down',
handler: DataController.tearDown,
options: {
description: 'Used to export the database and upload the file to our AWS S3 bucket',
description: 'Used to remove the acceptance test data from the database',
app: { excludeFromProd: true }
}
}
Expand Down
28 changes: 28 additions & 0 deletions app/services/data/seed/seed.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict'

/**
* Runs the Knex seed process programmatically
* @module SeedService
*/

const { db } = require('../../../../db/db.js')

/**
* Triggers the Knex seed process programmatically
*
* This is the same as calling `knex seed:run` on the command line. Only we pull in `db.js` because that is our file
* which sets up Knex with the right config and all our 'tweaks'.
*
* In this context you can read `db.seed.run()` as `knex.seed.run()`.
*
* See {@link https://knexjs.org/guide/migrations.html#seed-files | Seed files} for more details.
*
* Credit to {@link https://stackoverflow.com/a/53169879 | Programmatically run knex seed:run}
*/
async function go () {
await db.seed.run()
}

module.exports = {
go
}
4 changes: 3 additions & 1 deletion config/database.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const config = {
password: process.env.POSTGRES_PASSWORD,
port: process.env.POSTGRES_PORT,
database: process.env.POSTGRES_DB,
testDatabase: process.env.POSTGRES_DB_TEST
testDatabase: process.env.POSTGRES_DB_TEST,
// Only used when seeding our dev/test user records
defaultUserPassword: process.env.DEFAULT_USER_PASSWORD
}

module.exports = config
141 changes: 141 additions & 0 deletions db/seeds/01-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'use strict'

const bcrypt = require('bcryptjs')
const { randomUUID } = require('crypto')

const DatabaseConfig = require('../../config/database.config.js')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could destructure defaultUserPassword if we wanted seeing as it's the only bit of DatabaseConfig we use here? Not going to insist on it though 😁

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (as in you have forced me to think why I did this!) when we are talking about a value as opposed to a function it's best to leave the object so you have some context when it appears in the code, for example, I'm using it in

function _generateHashedPassword () {
  const salt = bcrypt.genSaltSync(10)

  return bcrypt.hashSync(DatabaseConfig.defaultUserPassword, salt)
}

Remove DatabaseConfig it starts looking a bit like it appears from nowhere.

function _generateHashedPassword () {
  const salt = bcrypt.genSaltSync(10)

  return bcrypt.hashSync(defaultUserPassword, salt)
}

I am conscious they are both just objects declared at the top of the file.

Again, I'm not precious about it either 🤷 . But it feels like a 'convention-in-the-making'. So, one to think about and discuss on the Dev call in my absence!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we are having a bit of a discussion about this 😁 I was wondering why we are generating the salt separately? I think we get the same result if we just do this return bcrypt.hashSync(defaultUserPassword, 10).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@Jozzey Jozzey Jul 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a look at Technique 2 in that readme 😉

I would guess that the technique to generate the salt separately would be more efficient if you were performing multiple hashes as you could reuse the salt to save a bit of CPU? Not fussed either way but thought I'd mention it since we were having a chat about that chunk of code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a valid point. I didn't dig that far because I was just trying to replicate what they had done so we generate passwords the rest of the service can recognise. But I think technique 2 makes the code just a bit simpler so leave this with me and I'll make the change.


const seedUsers = [
{
userName: '[email protected]',
application: 'water_admin',
group: 'super'
},
{
userName: '[email protected]',
application: 'water_admin',
group: 'super'
},
{
userName: '[email protected]',
application: 'water_admin',
group: 'environment_officer'
},
{
userName: '[email protected]',
application: 'water_admin',
group: 'wirs'
},
{
userName: '[email protected]',
application: 'water_admin',
group: 'billing_and_data'
},
{
userName: '[email protected]',
application: 'water_admin',
group: 'psc'
},
{
userName: '[email protected]',
application: 'water_vml'
},
{
userName: '[email protected]',
application: 'water_vml'
},
{
userName: '[email protected]',
application: 'water_vml'
}
]

async function seed (knex) {
await _insertUsersWhereNotExists(knex)

await _updateSeedUsersWithUserIdAndGroupId(knex)

await _insertUserGroupsWhereNotExists(knex)
}

function _generateHashedPassword () {
// 10 is the number of salt rounds to perform to generate the salt. The legacy code uses
// const salt = bcrypt.genSaltSync(10) to pre-generate the salt before passing it to hashSync(). But this is
// intended for operations where you need to hash a large number of values. If you just pass in a number bcrypt will
// autogenerate the salt for you.
// https://github.com/kelektiv/node.bcrypt.js#usage
return bcrypt.hashSync(DatabaseConfig.defaultUserPassword, 10)
}

async function _groups (knex) {
return knex('idm.groups')
.select('groupId', 'group')
}

async function _insertUsersWhereNotExists (knex) {
const password = _generateHashedPassword()

for (const seedUser of seedUsers) {
const existingUser = await knex('idm.users')
.first('userId')
.where('userName', seedUser.userName)
.andWhere('application', seedUser.application)

if (!existingUser) {
await knex('idm.users')
.insert({
userName: seedUser.userName,
application: seedUser.application,
password,
userData: '{ "source": "Seeded" }',
resetRequired: 0,
badLogins: 0
})
}
}
}

async function _insertUserGroupsWhereNotExists (knex) {
const seedUsersWithGroups = seedUsers.filter((seedData) => seedData.group)

for (const seedUser of seedUsersWithGroups) {
const existingUserGroup = await knex('idm.userGroups')
.select('userGroupId')
.where('userId', seedUser.userId)
.andWhere('groupId', seedUser.groupId)

if (!existingUserGroup) {
await knex('idm.userGroups')
.insert({
userGroupId: randomUUID({ disableEntropyCache: true }),
userId: seedUser.userId,
groupId: seedUser.groupId
})
}
}
}

async function _updateSeedUsersWithUserIdAndGroupId (knex) {
const users = await _users(knex)
const groups = await _groups(knex)

seedUsers.forEach((seedUser) => {
const user = users.find(({ userName }) => userName === seedUser.userName)
seedUser.userId = user.userId

if (seedUser.group) {
const userGroup = groups.find(({ group }) => group === seedUser.group)
seedUser.groupId = userGroup.groupId
}
})
}

async function _users (knex) {
return knex('idm.users')
.select('userId', 'userName')
.whereJsonPath('userData', '$.source', '=', 'Seeded')
}

module.exports = {
seed
}
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"migrate:db:test": "NODE_ENV=test knex migrate:latest",
"rollback:db": "knex migrate:rollback --all",
"rollback:db:test": "NODE_ENV=test knex migrate:rollback --all",
"seed:db": "knex seed:run --knexfile knexfile.application.js",
"lint": "standard",
"test": "lab --silent-skips --shuffle",
"postinstall": "npm run build",
Expand All @@ -30,6 +31,7 @@
"@hapi/hapi": "^21.1.0",
"@hapi/inert": "^7.0.0",
"@hapi/vision": "^7.0.0",
"bcryptjs": "^2.4.3",
"blipp": "^4.0.2",
"dotenv": "^16.0.3",
"got": "^12.5.3",
Expand Down
Loading