UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

271 lines • 9.2 kB
import User from '../../types/user.js'; import NotFoundError from '../../error/notfound-error.js'; const TABLE = 'users'; const PASSWORD_HASH_TABLE = 'used_passwords'; const USER_COLUMNS_PUBLIC = [ 'id', 'name', 'username', 'email', 'image_url', 'seen_at', 'is_service', 'scim_id', ]; const USER_COLUMNS = [...USER_COLUMNS_PUBLIC, 'login_attempts', 'created_at']; const emptify = (value) => { if (!value) { return undefined; } return value; }; const safeToLower = (s) => (s ? s.toLowerCase() : s); const mapUserToColumns = (user) => ({ name: user.name, username: user.username, email: safeToLower(user.email), image_url: user.imageUrl, }); 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, seenAt: row.seen_at, createdAt: row.created_at, isService: row.is_service, scimId: row.scim_id, }); }; export class UserStore { constructor(db, getLogger) { this.db = db; this.logger = getLogger('user-store.ts'); } async getPasswordsPreviouslyUsed(userId) { const previouslyUsedPasswords = await this.db(PASSWORD_HASH_TABLE) .select('password_hash') .where({ user_id: userId }); return previouslyUsedPasswords.map((row) => row.password_hash); } async deletePasswordsUsedMoreThanNTimesAgo(userId, keepLastN) { await this.db.raw(` WITH UserPasswords AS ( SELECT user_id, password_hash, used_at, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY used_at DESC) AS rn FROM ${PASSWORD_HASH_TABLE} WHERE user_id = ?) DELETE FROM ${PASSWORD_HASH_TABLE} WHERE user_id = ? AND (user_id, password_hash, used_at) NOT IN (SELECT user_id, password_hash, used_at FROM UserPasswords WHERE rn <= ? ); `, [userId, userId, keepLastN]); } async update(id, fields) { await this.activeUsers() .where('id', id) .update(mapUserToColumns(fields)); return this.get(id); } async insert(user) { const emailHash = user.email ? this.db.raw(`encode(sha256(?::bytea), 'hex')`, [user.email]) : null; const rows = await this.db(TABLE) .insert({ ...mapUserToColumns(user), email_hash: emailHash, created_at: new Date(), }) .returning(USER_COLUMNS); return rowToUser(rows[0]); } async upsert(user) { const id = await this.hasUser(user); if (id) { return this.update(id, user); } return this.insert(user); } buildSelectUser(q) { const query = this.activeAll(); if (q.id) { return query.where('id', q.id); } if (q.email) { return query.where('email', safeToLower(q.email)); } if (q.username) { return query.where('username', q.username); } throw new Error('Can only find users with id, username or email.'); } activeAll() { return this.db(TABLE).where({ deleted_at: null, }); } activeUsers() { return this.db(TABLE).where({ deleted_at: null, is_service: false, is_system: false, }); } async hasUser(idQuery) { const query = this.buildSelectUser(idQuery); const item = await query.first('id'); return item ? item.id : undefined; } async getAll(params) { const usersQuery = this.activeUsers().select(USER_COLUMNS); if (params) { if (params.sortBy) { usersQuery.orderBy(params.sortBy, params.sortOrder); } if (params.limit) { usersQuery.limit(params.limit); } if (params.offset) { usersQuery.offset(params.offset); } } return (await usersQuery).map(rowToUser); } async search(query) { const users = await this.activeUsers() .select(USER_COLUMNS_PUBLIC) .where('name', 'ILIKE', `%${query}%`) .orWhere('username', 'ILIKE', `${query}%`) .orWhere('email', 'ILIKE', `${query}%`); return users.map(rowToUser); } async getAllWithId(userIdList) { const users = await this.activeUsers() .select(USER_COLUMNS_PUBLIC) .whereIn('id', userIdList); return users.map(rowToUser); } async getByQuery(idQuery) { const row = await this.buildSelectUser(idQuery).first(USER_COLUMNS); return rowToUser(row); } async delete(id) { await this.activeUsers() .where({ id }) .update({ deleted_at: new Date(), // @ts-expect-error email is non-nullable in User type email: null, // @ts-expect-error username is non-nullable in User type username: null, scim_id: null, scim_external_id: null, name: this.db.raw('name || ?', '(Deleted)'), }); } async getPasswordHash(userId) { const item = await this.activeUsers() .where('id', userId) .first('password_hash'); if (!item) { throw new NotFoundError('User not found'); } return item.password_hash; } async setPasswordHash(userId, passwordHash, disallowNPreviousPasswords) { await this.activeUsers().where('id', userId).update({ // @ts-expect-error password_hash does not exist in User type password_hash: passwordHash, }); // We apparently set this to null, but you should be allowed to have null, so need to allow this if (passwordHash) { await this.db(PASSWORD_HASH_TABLE).insert({ user_id: userId, password_hash: passwordHash, }); await this.deletePasswordsUsedMoreThanNTimesAgo(userId, disallowNPreviousPasswords); } } async incLoginAttempts(user) { await this.buildSelectUser(user).increment('login_attempts', 1); } async successfullyLogin(user) { const currentDate = new Date(); const updateQuery = this.buildSelectUser(user).update({ // @ts-expect-error login_attempts does not exist in User type login_attempts: 0, seen_at: currentDate, }); let firstLoginOrder = 0; const existingUser = await this.buildSelectUser(user).first('first_seen_at'); if (!existingUser.first_seen_at) { const countEarlierUsers = await this.db(TABLE) .whereNotNull('first_seen_at') .andWhere('first_seen_at', '<', currentDate) .count('*') .then((res) => Number(res[0].count)); firstLoginOrder = countEarlierUsers; await updateQuery.update({ // @ts-expect-error first_seen_at does not exist in User type first_seen_at: currentDate, }); } await updateQuery; return firstLoginOrder; } async deleteAll() { await this.activeUsers().del(); } async deleteScimUsers() { const rows = await this.db(TABLE) .whereNotNull('scim_id') .del() .returning(USER_COLUMNS); return rows.map(rowToUser); } async count() { return this.activeUsers() .count('*') .then((res) => Number(res[0].count)); } async countServiceAccounts() { return this.db(TABLE) .where({ deleted_at: null, is_service: true, }) .count('*') .then((res) => Number(res[0].count)); } async countRecentlyDeleted() { return this.db(TABLE) .whereNotNull('deleted_at') .andWhere('deleted_at', '>=', this.db.raw(`NOW() - INTERVAL '1 month'`)) .andWhere({ is_service: false, is_system: false }) .count('*') .then((res) => Number(res[0].count)); } destroy() { } async exists(id) { const result = await this.db.raw(`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ? and deleted_at = null) AS present`, [id]); const { present } = result.rows[0]; return present; } async get(id) { const row = await this.activeUsers().where({ id }).first(); return rowToUser(row); } async getFirstUserDate() { const firstInstanceUser = await this.db('users') .select('created_at') .where('is_system', '=', false) .orderBy('created_at', 'asc') .first(); return firstInstanceUser ? firstInstanceUser.created_at : null; } } //# sourceMappingURL=user-store.js.map