mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-11 00:08:30 +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