mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +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 ClientApplicationsStore = require('./client-applications-store');
|
||||||
const ContextFieldStore = require('./context-field-store');
|
const ContextFieldStore = require('./context-field-store');
|
||||||
const SettingStore = require('./setting-store');
|
const SettingStore = require('./setting-store');
|
||||||
|
const UserStore = require('./user-store');
|
||||||
|
|
||||||
module.exports.createStores = (config, eventBus) => {
|
module.exports.createStores = (config, eventBus) => {
|
||||||
const { getLogger } = config;
|
const { getLogger } = config;
|
||||||
@ -40,5 +41,6 @@ module.exports.createStores = (config, eventBus) => {
|
|||||||
getLogger,
|
getLogger,
|
||||||
),
|
),
|
||||||
settingStore: new SettingStore(db, 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';
|
'use strict';
|
||||||
|
|
||||||
function extractUsername(req) {
|
function extractUsername(req) {
|
||||||
return req.user ? req.user.email : 'unknown';
|
return req.user ? req.user.email || req.user.username : 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = extractUsername;
|
module.exports = extractUsername;
|
||||||
|
10
lib/user.js
10
lib/user.js
@ -5,12 +5,15 @@ const Joi = require('@hapi/joi');
|
|||||||
|
|
||||||
module.exports = class User {
|
module.exports = class User {
|
||||||
constructor({
|
constructor({
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
username,
|
username,
|
||||||
systemId,
|
|
||||||
imageUrl,
|
imageUrl,
|
||||||
permissions,
|
permissions,
|
||||||
|
seenAt,
|
||||||
|
loginAttempts,
|
||||||
|
createdAt,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!username && !email) {
|
if (!username && !email) {
|
||||||
throw new TypeError('Username or Email us reuqired');
|
throw new TypeError('Username or Email us reuqired');
|
||||||
@ -19,12 +22,15 @@ module.exports = class User {
|
|||||||
Joi.assert(username, Joi.string(), 'Username');
|
Joi.assert(username, Joi.string(), 'Username');
|
||||||
Joi.assert(name, Joi.string(), 'Name');
|
Joi.assert(name, Joi.string(), 'Name');
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.systemId = systemId;
|
|
||||||
this.permissions = permissions;
|
this.permissions = permissions;
|
||||||
this.imageUrl = imageUrl || this.generateImageUrl();
|
this.imageUrl = imageUrl || this.generateImageUrl();
|
||||||
|
this.seenAt = seenAt;
|
||||||
|
this.loginAttempts = loginAttempts;
|
||||||
|
this.createdAt = createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateImageUrl() {
|
generateImageUrl() {
|
||||||
|
@ -66,3 +66,8 @@ test('Should create user with only username defined', t => {
|
|||||||
'https://gravatar.com/avatar/140fd5a002fb8d728a9848f8c9fcea2a?size=42&default=retro',
|
'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_applications').del(),
|
||||||
stores.db('client_instances').del(),
|
stores.db('client_instances').del(),
|
||||||
stores.db('context_fields').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