1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-06 00:07:44 +01:00
unleash.unleash/src/lib/db/account-store.ts
David Leek 1dadd23594
chore:system user and events created by userid migrations (#5612)
## About the changes

Migrations for:
- Adds column is_system to users
- Inserts unleash_system_user id -1337 to users 

includes `is_system: false` in the activeUsers and activeAccounts where filter

Tested by running:
`
select * into users_pre_check from users where id > -1;
delete from users where id > -1;
`
before starting unleash, then inspecting users table after unleash has
started and verifying that an 'admin' user has been created.

---------

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
2023-12-22 11:19:39 +01:00

202 lines
5.9 KiB
TypeScript

import { Logger, LogProvider } from '../logger';
import User from '../types/user';
import NotFoundError from '../error/notfound-error';
import { IUserLookup } from '../types/stores/user-store';
import { IAdminCount } from '../types/stores/account-store';
import { IAccountStore } from '../types';
import { Db } from './db';
const TABLE = 'users';
const USER_COLUMNS_PUBLIC = [
'id',
'name',
'username',
'email',
'image_url',
'seen_at',
'is_service',
];
const USER_COLUMNS = [...USER_COLUMNS_PUBLIC, 'login_attempts', 'created_at'];
const emptify = (value) => {
if (!value) {
return undefined;
}
return value;
};
const safeToLower = (s?: string) => (s ? s.toLowerCase() : s);
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,
});
};
export class AccountStore implements IAccountStore {
private db: Db;
private logger: Logger;
constructor(db: Db, getLogger: LogProvider) {
this.db = db;
this.logger = getLogger('account-store.ts');
}
buildSelectAccount(q: IUserLookup): any {
const query = this.activeAccounts();
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.');
}
activeAccounts(): any {
return this.db(TABLE).where({
deleted_at: null,
is_system: false,
});
}
async hasAccount(idQuery: IUserLookup): Promise<number | undefined> {
const query = this.buildSelectAccount(idQuery);
const item = await query.first('id');
return item ? item.id : undefined;
}
async getAll(): Promise<User[]> {
const users = await this.activeAccounts().select(USER_COLUMNS);
return users.map(rowToUser);
}
async search(query: string): Promise<User[]> {
const users = await this.activeAccounts()
.select(USER_COLUMNS_PUBLIC)
.where('name', 'ILIKE', `%${query}%`)
.orWhere('username', 'ILIKE', `${query}%`)
.orWhere('email', 'ILIKE', `${query}%`);
return users.map(rowToUser);
}
async getAllWithId(userIdList: number[]): Promise<User[]> {
const users = await this.activeAccounts()
.select(USER_COLUMNS_PUBLIC)
.whereIn('id', userIdList);
return users.map(rowToUser);
}
async getByQuery(idQuery: IUserLookup): Promise<User> {
const row = await this.buildSelectAccount(idQuery).first(USER_COLUMNS);
return rowToUser(row);
}
async delete(id: number): Promise<void> {
return this.activeAccounts()
.where({ id })
.update({
deleted_at: new Date(),
email: null,
username: null,
name: this.db.raw('name || ?', '(Deleted)'),
});
}
async deleteAll(): Promise<void> {
await this.activeAccounts().del();
}
async count(): Promise<number> {
return this.activeAccounts()
.count('*')
.then((res) => Number(res[0].count));
}
destroy(): void {}
async exists(id: number): Promise<boolean> {
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: number): Promise<User> {
const row = await this.activeAccounts().where({ id }).first();
return rowToUser(row);
}
async getAccountByPersonalAccessToken(secret: string): Promise<User> {
const row = await this.activeAccounts()
.select(USER_COLUMNS.map((column) => `${TABLE}.${column}`))
.leftJoin(
'personal_access_tokens',
'personal_access_tokens.user_id',
`${TABLE}.id`,
)
.where('secret', secret)
.andWhere('expires_at', '>', 'now()')
.first();
return rowToUser(row);
}
async markSeenAt(secrets: string[]): Promise<void> {
const now = new Date();
try {
await this.db('personal_access_tokens')
.whereIn('secret', secrets)
.update({ seen_at: now });
} catch (err) {
this.logger.error('Could not update lastSeen, error: ', err);
}
}
async getAdminCount(): Promise<IAdminCount> {
const adminCount = await this.activeAccounts()
.join('role_user as ru', 'users.id', 'ru.user_id')
.where(
'ru.role_id',
'=',
this.db.raw('(SELECT id FROM roles WHERE name = ?)', ['Admin']),
)
.select(
this.db.raw(
'COUNT(CASE WHEN users.password_hash IS NOT NULL AND users.is_service = false THEN 1 END)::integer AS password',
),
this.db.raw(
'COUNT(CASE WHEN users.password_hash IS NULL AND users.is_service = false THEN 1 END)::integer AS no_password',
),
this.db.raw(
'COUNT(CASE WHEN users.is_service = true THEN 1 END)::integer AS service',
),
);
return {
password: adminCount[0].password,
noPassword: adminCount[0].no_password,
service: adminCount[0].service,
};
}
}