1
0
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:
Ivar Conradi Østhus 2020-05-12 23:05:26 +02:00 committed by GitHub
parent 49e0c0fa29
commit 5675f99e78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 363 additions and 3 deletions

View File

@ -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
View 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;

View File

@ -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;

View File

@ -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() {

View File

@ -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');
});

View 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,
);
};

View File

@ -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(),
]);
}

View 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]);
});