mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	fix: add user-store (#590)
This commit is contained in:
		
							parent
							
								
									49e0c0fa29
								
							
						
					
					
						commit
						5675f99e78
					
				| @ -10,6 +10,7 @@ const ClientMetricsStore = require('./client-metrics-store'); | ||||
| const ClientApplicationsStore = require('./client-applications-store'); | ||||
| const ContextFieldStore = require('./context-field-store'); | ||||
| const SettingStore = require('./setting-store'); | ||||
| const UserStore = require('./user-store'); | ||||
| 
 | ||||
| module.exports.createStores = (config, eventBus) => { | ||||
|     const { getLogger } = config; | ||||
| @ -40,5 +41,6 @@ module.exports.createStores = (config, eventBus) => { | ||||
|             getLogger, | ||||
|         ), | ||||
|         settingStore: new SettingStore(db, getLogger), | ||||
|         userStore: new UserStore(db, getLogger), | ||||
|     }; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										153
									
								
								lib/db/user-store.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								lib/db/user-store.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,153 @@ | ||||
| /* eslint camelcase: "off" */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| const NotFoundError = require('../error/notfound-error'); | ||||
| const User = require('../user'); | ||||
| 
 | ||||
| const TABLE = 'users'; | ||||
| 
 | ||||
| const USER_COLUMNS = [ | ||||
|     'id', | ||||
|     'name', | ||||
|     'username', | ||||
|     'email', | ||||
|     'image_url', | ||||
|     'permissions', | ||||
|     'login_attempts', | ||||
|     'seen_at', | ||||
|     'created_at', | ||||
| ]; | ||||
| 
 | ||||
| const emptify = value => { | ||||
|     if (!value) { | ||||
|         return undefined; | ||||
|     } | ||||
|     return value; | ||||
| }; | ||||
| 
 | ||||
| const mapUserToColumns = user => ({ | ||||
|     name: user.name, | ||||
|     username: user.username, | ||||
|     email: user.email, | ||||
|     image_url: user.imageUrl, | ||||
|     permissions: user.permissions ? JSON.stringify(user.permissions) : null, | ||||
| }); | ||||
| 
 | ||||
| const rowToUser = row => { | ||||
|     if (!row) { | ||||
|         throw new NotFoundError('No user found'); | ||||
|     } | ||||
|     return new User({ | ||||
|         id: row.id, | ||||
|         name: emptify(row.name), | ||||
|         username: emptify(row.username), | ||||
|         email: emptify(row.email), | ||||
|         imageUrl: emptify(row.image_url), | ||||
|         loginAttempts: row.login_attempts, | ||||
|         permissions: row.permissions, | ||||
|         seenAt: row.seen_at, | ||||
|         createdAt: row.created_at, | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| class UserStore { | ||||
|     constructor(db, getLogger) { | ||||
|         this.db = db; | ||||
|         this.logger = getLogger('user-store.js'); | ||||
|     } | ||||
| 
 | ||||
|     async update(id, user) { | ||||
|         await this.db(TABLE) | ||||
|             .where('id', id) | ||||
|             .update(mapUserToColumns(user)); | ||||
|         return this.get({ id }); | ||||
|     } | ||||
| 
 | ||||
|     async insert(user) { | ||||
|         const [id] = await this.db(TABLE) | ||||
|             .insert(mapUserToColumns(user)) | ||||
|             .returning('id'); | ||||
|         return this.get({ id }); | ||||
|     } | ||||
| 
 | ||||
|     buildSelectUser(q) { | ||||
|         const query = this.db(TABLE); | ||||
|         if (q.id) { | ||||
|             return query.where('id', q.id); | ||||
|         } | ||||
|         if (q.email) { | ||||
|             return query.where('email', q.email); | ||||
|         } | ||||
|         if (q.username) { | ||||
|             return query.where('username', q.username); | ||||
|         } | ||||
|         throw new Error('Can only find users with id, username or email.'); | ||||
|     } | ||||
| 
 | ||||
|     async hasUser(idQuery) { | ||||
|         const query = this.buildSelectUser(idQuery); | ||||
|         const item = await query.first('id'); | ||||
|         return item ? item.id : undefined; | ||||
|     } | ||||
| 
 | ||||
|     async upsert(user) { | ||||
|         const id = await this.hasUser(user); | ||||
| 
 | ||||
|         if (id) { | ||||
|             return this.update(id, user); | ||||
|         } | ||||
|         return this.insert(user); | ||||
|     } | ||||
| 
 | ||||
|     async getAll() { | ||||
|         const users = await this.db.select(USER_COLUMNS).from(TABLE); | ||||
|         return users.map(rowToUser); | ||||
|     } | ||||
| 
 | ||||
|     async get(idQuery) { | ||||
|         const row = await this.buildSelectUser(idQuery).first(USER_COLUMNS); | ||||
|         return rowToUser(row); | ||||
|     } | ||||
| 
 | ||||
|     async delete(id) { | ||||
|         return this.db(TABLE) | ||||
|             .where({ id }) | ||||
|             .del(); | ||||
|     } | ||||
| 
 | ||||
|     async getPasswordHash(userId) { | ||||
|         const item = await this.db(TABLE) | ||||
|             .where('id', userId) | ||||
|             .first('password_hash'); | ||||
| 
 | ||||
|         if (!item) { | ||||
|             throw new NotFoundError('User not found'); | ||||
|         } | ||||
| 
 | ||||
|         return item.password_hash; | ||||
|     } | ||||
| 
 | ||||
|     async setPasswordHash(userId, passwordHash) { | ||||
|         return this.db(TABLE) | ||||
|             .where('id', userId) | ||||
|             .update({ | ||||
|                 password_hash: passwordHash, | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     async incLoginAttempts(user) { | ||||
|         return this.buildSelectUser(user).increment({ | ||||
|             login_attempts: 1, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     async succesfullLogin(user) { | ||||
|         return this.buildSelectUser(user).update({ | ||||
|             login_attempts: 0, | ||||
|             seen_at: new Date(), | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = UserStore; | ||||
| @ -1,7 +1,7 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| function extractUsername(req) { | ||||
|     return req.user ? req.user.email : 'unknown'; | ||||
|     return req.user ? req.user.email || req.user.username : 'unknown'; | ||||
| } | ||||
| 
 | ||||
| module.exports = extractUsername; | ||||
|  | ||||
							
								
								
									
										10
									
								
								lib/user.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								lib/user.js
									
									
									
									
									
								
							| @ -5,12 +5,15 @@ const Joi = require('@hapi/joi'); | ||||
| 
 | ||||
| module.exports = class User { | ||||
|     constructor({ | ||||
|         id, | ||||
|         name, | ||||
|         email, | ||||
|         username, | ||||
|         systemId, | ||||
|         imageUrl, | ||||
|         permissions, | ||||
|         seenAt, | ||||
|         loginAttempts, | ||||
|         createdAt, | ||||
|     } = {}) { | ||||
|         if (!username && !email) { | ||||
|             throw new TypeError('Username or Email us reuqired'); | ||||
| @ -19,12 +22,15 @@ module.exports = class User { | ||||
|         Joi.assert(username, Joi.string(), 'Username'); | ||||
|         Joi.assert(name, Joi.string(), 'Name'); | ||||
| 
 | ||||
|         this.id = id; | ||||
|         this.name = name; | ||||
|         this.username = username; | ||||
|         this.email = email; | ||||
|         this.systemId = systemId; | ||||
|         this.permissions = permissions; | ||||
|         this.imageUrl = imageUrl || this.generateImageUrl(); | ||||
|         this.seenAt = seenAt; | ||||
|         this.loginAttempts = loginAttempts; | ||||
|         this.createdAt = createdAt; | ||||
|     } | ||||
| 
 | ||||
|     generateImageUrl() { | ||||
|  | ||||
| @ -66,3 +66,8 @@ test('Should create user with only username defined', t => { | ||||
|         'https://gravatar.com/avatar/140fd5a002fb8d728a9848f8c9fcea2a?size=42&default=retro', | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| test('Should create user with only username defined and undefined email', t => { | ||||
|     const user = new User({ username: 'some-user', email: undefined }); | ||||
|     t.is(user.username, 'some-user'); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										24
									
								
								migrations/20200429175747-users-settings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								migrations/20200429175747-users-settings.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| exports.up = function(db, callback) { | ||||
|     db.runSql( | ||||
|         ` | ||||
|         ALTER TABLE users ADD "settings" json; | ||||
|         ALTER TABLE users ADD "permissions" json; | ||||
|         ALTER TABLE users ALTER COLUMN "permissions" SET DEFAULT '[]'; | ||||
|         ALTER TABLE users DROP COLUMN "system_id"; | ||||
|     `,
 | ||||
|         callback, | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| exports.down = function(db, callback) { | ||||
|     db.runSql( | ||||
|         ` | ||||
|       ALTER TABLE users DROP COLUMN "settings"; | ||||
|       ALTER TABLE users DROP COLUMN "permissions"; | ||||
|       ALTER TABLE users ADD COLUMN "system_id" VARCHAR; | ||||
|     `,
 | ||||
|         callback, | ||||
|     ); | ||||
| }; | ||||
| @ -23,6 +23,7 @@ async function resetDatabase(stores) { | ||||
|         stores.db('client_applications').del(), | ||||
|         stores.db('client_instances').del(), | ||||
|         stores.db('context_fields').del(), | ||||
|         stores.db('users').del(), | ||||
|     ]); | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										169
									
								
								test/e2e/stores/user-store.e2e.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								test/e2e/stores/user-store.e2e.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,169 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const test = require('ava'); | ||||
| const User = require('../../../lib/user'); | ||||
| const { | ||||
|     CREATE_FEATURE, | ||||
|     DELETE_FEATURE, | ||||
|     UPDATE_FEATURE, | ||||
| } = require('../../../lib/permissions'); | ||||
| const NotFoundError = require('../../../lib/error/notfound-error'); | ||||
| const dbInit = require('../helpers/database-init'); | ||||
| const getLogger = require('../../fixtures/no-logger'); | ||||
| 
 | ||||
| let stores; | ||||
| 
 | ||||
| test.before(async () => { | ||||
|     const db = await dbInit('user_store_serial', getLogger); | ||||
|     stores = db.stores; | ||||
| }); | ||||
| 
 | ||||
| test.after(async () => { | ||||
|     await stores.db.destroy(); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should have no users', async t => { | ||||
|     const users = await stores.userStore.getAll(); | ||||
|     t.deepEqual(users, []); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should insert new user with email', async t => { | ||||
|     const user = new User({ email: 'me2@mail.com' }); | ||||
|     await stores.userStore.upsert(user); | ||||
|     const users = await stores.userStore.getAll(); | ||||
|     t.deepEqual(users[0].email, user.email); | ||||
|     t.truthy(users[0].id); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should not allow two users with same email', async t => { | ||||
|     const error = await t.throwsAsync( | ||||
|         async () => { | ||||
|             await stores.userStore.insert({ email: 'me2@mail.com' }); | ||||
|             await stores.userStore.insert({ email: 'me2@mail.com' }); | ||||
|         }, | ||||
|         { instanceOf: Error }, | ||||
|     ); | ||||
| 
 | ||||
|     t.true( | ||||
|         error.message.includes( | ||||
|             'duplicate key value violates unique constraint', | ||||
|         ), | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should insert new user with email and return it', async t => { | ||||
|     const user = new User({ email: 'me2@mail.com' }); | ||||
|     const newUser = await stores.userStore.upsert(user); | ||||
|     t.deepEqual(newUser.email, user.email); | ||||
|     t.truthy(newUser.id); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should insert new user with username', async t => { | ||||
|     const user = new User({ username: 'admin' }); | ||||
|     await stores.userStore.upsert(user); | ||||
|     const dbUser = await stores.userStore.get(user); | ||||
|     t.deepEqual(dbUser.username, user.username); | ||||
| }); | ||||
| 
 | ||||
| test('Should require email or username', async t => { | ||||
|     const error = await t.throwsAsync( | ||||
|         async () => { | ||||
|             await stores.userStore.upsert({}); | ||||
|         }, | ||||
|         { instanceOf: Error }, | ||||
|     ); | ||||
| 
 | ||||
|     t.is(error.message, 'Can only find users with id, username or email.'); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should set password_hash for user', async t => { | ||||
|     const store = stores.userStore; | ||||
|     const user = await store.insert(new User({ email: 'admin@mail.com' })); | ||||
|     await store.setPasswordHash(user.id, 'rubbish'); | ||||
|     const hash = await store.getPasswordHash(user.id); | ||||
| 
 | ||||
|     t.is(hash, 'rubbish'); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should not get password_hash for unknown userId', async t => { | ||||
|     const store = stores.userStore; | ||||
|     const error = await t.throwsAsync( | ||||
|         async () => { | ||||
|             await store.getPasswordHash(-12); | ||||
|         }, | ||||
|         { instanceOf: NotFoundError }, | ||||
|     ); | ||||
| 
 | ||||
|     t.is(error.message, 'User not found'); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should update loginAttempts for user', async t => { | ||||
|     const store = stores.userStore; | ||||
|     const user = new User({ email: 'admin@mail.com' }); | ||||
|     await store.upsert(user); | ||||
|     await store.incLoginAttempts(user); | ||||
|     await store.incLoginAttempts(user); | ||||
|     const storedUser = await store.get(user); | ||||
| 
 | ||||
|     t.is(storedUser.loginAttempts, 2); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should not increment for user unknwn user', async t => { | ||||
|     const store = stores.userStore; | ||||
|     const user = new User({ email: 'another@mail.com' }); | ||||
|     await store.upsert(user); | ||||
|     await store.incLoginAttempts(new User({ email: 'unknown@mail.com' })); | ||||
|     const storedUser = await store.get(user); | ||||
| 
 | ||||
|     t.is(storedUser.loginAttempts, 0); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should reset user after successful login', async t => { | ||||
|     const store = stores.userStore; | ||||
|     const user = await store.insert( | ||||
|         new User({ email: 'anotherWithResert@mail.com' }), | ||||
|     ); | ||||
|     await store.incLoginAttempts(user); | ||||
|     await store.incLoginAttempts(user); | ||||
| 
 | ||||
|     await store.succesfullLogin(user); | ||||
|     const storedUser = await store.get(user); | ||||
| 
 | ||||
|     t.is(storedUser.loginAttempts, 0); | ||||
|     t.true(storedUser.seenAt >= user.seenAt); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should store and get permsissions', async t => { | ||||
|     const store = stores.userStore; | ||||
|     const email = 'userWithPermissions@mail.com'; | ||||
|     const user = new User({ | ||||
|         email, | ||||
|         permissions: [CREATE_FEATURE, UPDATE_FEATURE, DELETE_FEATURE], | ||||
|     }); | ||||
| 
 | ||||
|     await store.upsert(user); | ||||
| 
 | ||||
|     const storedUser = await store.get({ email }); | ||||
| 
 | ||||
|     t.deepEqual(storedUser.permissions, user.permissions); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should only update specified fields on user', async t => { | ||||
|     const store = stores.userStore; | ||||
|     const email = 'userTobeUpdated@mail.com'; | ||||
|     const user = new User({ | ||||
|         email, | ||||
|         username: 'test', | ||||
|         permissions: [CREATE_FEATURE, UPDATE_FEATURE, DELETE_FEATURE], | ||||
|     }); | ||||
| 
 | ||||
|     await store.upsert(user); | ||||
| 
 | ||||
|     await store.upsert({ username: 'test', permissions: [CREATE_FEATURE] }); | ||||
| 
 | ||||
|     const storedUser = await store.get({ email }); | ||||
| 
 | ||||
|     t.deepEqual(storedUser.email, user.email); | ||||
|     t.deepEqual(storedUser.username, user.username); | ||||
|     t.deepEqual(storedUser.permissions, [CREATE_FEATURE]); | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user