mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
feat: add the account abstraction logic (#2918)
https://linear.app/unleash/issue/2-579/improve-user-like-behaviour-for-service-accounts-accounts-concept Builds on top of https://github.com/Unleash/unleash/pull/2917 by moving the responsibility of handling both account types from `users` to `accounts`. Ideally: - `users` - Should only handle users; - `service-accounts` - Should only handle service accounts; - `accounts` - Should handle any type of account; This should hopefully also provide a good building block in case we later decide to refactor this further down the `accounts` path.
This commit is contained in:
parent
bb20c6d102
commit
7d73d772df
@ -150,6 +150,11 @@ export const ProjectAccessAssign = ({
|
|||||||
user.id === id && type === ENTITY_TYPE.USER
|
user.id === id && type === ENTITY_TYPE.USER
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.sort((a: IUser, b: IUser) => {
|
||||||
|
const aName = a.name || a.username || '';
|
||||||
|
const bName = b.name || b.username || '';
|
||||||
|
return aName.localeCompare(bName);
|
||||||
|
})
|
||||||
.map((user: IUser) => ({
|
.map((user: IUser) => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
entity: user,
|
entity: user,
|
||||||
@ -165,6 +170,11 @@ export const ProjectAccessAssign = ({
|
|||||||
type === ENTITY_TYPE.SERVICE_ACCOUNT
|
type === ENTITY_TYPE.SERVICE_ACCOUNT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.sort((a: IServiceAccount, b: IServiceAccount) => {
|
||||||
|
const aName = a.name || a.username || '';
|
||||||
|
const bName = b.name || b.username || '';
|
||||||
|
return aName.localeCompare(bName);
|
||||||
|
})
|
||||||
.map((serviceAccount: IServiceAccount) => ({
|
.map((serviceAccount: IServiceAccount) => ({
|
||||||
id: serviceAccount.id,
|
id: serviceAccount.id,
|
||||||
entity: serviceAccount,
|
entity: serviceAccount,
|
||||||
|
@ -22,7 +22,7 @@ export const useAccess = (): IUseAccessOutput => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
users: (data?.users as IUser[])?.filter(
|
users: (data?.users as IUser[])?.filter(
|
||||||
({ accountType }) => accountType === 'User'
|
({ accountType }) => !accountType || accountType === 'User'
|
||||||
),
|
),
|
||||||
serviceAccounts: (data?.users as IServiceAccount[])?.filter(
|
serviceAccounts: (data?.users as IServiceAccount[])?.filter(
|
||||||
({ accountType }) => accountType === 'Service Account'
|
({ accountType }) => accountType === 'Service Account'
|
||||||
|
@ -66,7 +66,7 @@ const useProjectAccess = (
|
|||||||
return formatAccessData({
|
return formatAccessData({
|
||||||
roles: data.roles,
|
roles: data.roles,
|
||||||
users: (data.users as IUser[]).filter(
|
users: (data.users as IUser[]).filter(
|
||||||
({ accountType }) => accountType === 'User'
|
({ accountType }) => !accountType || accountType === 'User'
|
||||||
),
|
),
|
||||||
serviceAccounts: (data.users as IUser[]).filter(
|
serviceAccounts: (data.users as IUser[]).filter(
|
||||||
({ accountType }) => accountType === 'Service Account'
|
({ accountType }) => accountType === 'Service Account'
|
||||||
|
172
src/lib/db/account-store.ts
Normal file
172
src/lib/db/account-store.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Logger, LogProvider } from '../logger';
|
||||||
|
import User from '../types/user';
|
||||||
|
|
||||||
|
import NotFoundError from '../error/notfound-error';
|
||||||
|
import { IUserLookup } from '../types/stores/user-store';
|
||||||
|
import { IAccountStore } from '../types';
|
||||||
|
|
||||||
|
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: Knex;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(db: Knex, 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,7 @@ import PatStore from './pat-store';
|
|||||||
import { PublicSignupTokenStore } from './public-signup-token-store';
|
import { PublicSignupTokenStore } from './public-signup-token-store';
|
||||||
import { FavoriteFeaturesStore } from './favorite-features-store';
|
import { FavoriteFeaturesStore } from './favorite-features-store';
|
||||||
import { FavoriteProjectsStore } from './favorite-projects-store';
|
import { FavoriteProjectsStore } from './favorite-projects-store';
|
||||||
|
import { AccountStore } from './account-store';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -57,6 +58,7 @@ export const createStores = (
|
|||||||
contextFieldStore: new ContextFieldStore(db, getLogger),
|
contextFieldStore: new ContextFieldStore(db, getLogger),
|
||||||
settingStore: new SettingStore(db, getLogger),
|
settingStore: new SettingStore(db, getLogger),
|
||||||
userStore: new UserStore(db, getLogger),
|
userStore: new UserStore(db, getLogger),
|
||||||
|
accountStore: new AccountStore(db, getLogger),
|
||||||
projectStore: new ProjectStore(
|
projectStore: new ProjectStore(
|
||||||
db,
|
db,
|
||||||
eventBus,
|
eventBus,
|
||||||
|
@ -126,11 +126,6 @@ class UserStore implements IUserStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<User[]> {
|
async getAll(): Promise<User[]> {
|
||||||
const users = await this.activeAll().select(USER_COLUMNS);
|
|
||||||
return users.map(rowToUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllUsers(): Promise<User[]> {
|
|
||||||
const users = await this.activeUsers().select(USER_COLUMNS);
|
const users = await this.activeUsers().select(USER_COLUMNS);
|
||||||
return users.map(rowToUser);
|
return users.map(rowToUser);
|
||||||
}
|
}
|
||||||
@ -145,7 +140,7 @@ class UserStore implements IUserStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllWithId(userIdList: number[]): Promise<User[]> {
|
async getAllWithId(userIdList: number[]): Promise<User[]> {
|
||||||
const users = await this.activeAll()
|
const users = await this.activeUsers()
|
||||||
.select(USER_COLUMNS_PUBLIC)
|
.select(USER_COLUMNS_PUBLIC)
|
||||||
.whereIn('id', userIdList);
|
.whereIn('id', userIdList);
|
||||||
return users.map(rowToUser);
|
return users.map(rowToUser);
|
||||||
@ -221,31 +216,6 @@ class UserStore implements IUserStore {
|
|||||||
const row = await this.activeUsers().where({ id }).first();
|
const row = await this.activeUsers().where({ id }).first();
|
||||||
return rowToUser(row);
|
return rowToUser(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByPersonalAccessToken(secret: string): Promise<User> {
|
|
||||||
const row = await this.activeAll()
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = UserStore;
|
module.exports = UserStore;
|
||||||
|
@ -14,11 +14,11 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not set user if unknown token', async () => {
|
test('should not set user if unknown token', async () => {
|
||||||
const userService = {
|
const accountService = {
|
||||||
getUserByPersonalAccessToken: jest.fn(),
|
getAccountByPersonalAccessToken: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const func = patMiddleware(config, { userService });
|
const func = patMiddleware(config, { accountService });
|
||||||
|
|
||||||
const cb = jest.fn();
|
const cb = jest.fn();
|
||||||
|
|
||||||
@ -35,11 +35,11 @@ test('should not set user if unknown token', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not set user if token wrong format', async () => {
|
test('should not set user if token wrong format', async () => {
|
||||||
const userService = {
|
const accountService = {
|
||||||
getUserByPersonalAccessToken: jest.fn(),
|
getAccountByPersonalAccessToken: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const func = patMiddleware(config, { userService });
|
const func = patMiddleware(config, { accountService });
|
||||||
|
|
||||||
const cb = jest.fn();
|
const cb = jest.fn();
|
||||||
|
|
||||||
@ -50,7 +50,9 @@ test('should not set user if token wrong format', async () => {
|
|||||||
|
|
||||||
await func(req, undefined, cb);
|
await func(req, undefined, cb);
|
||||||
|
|
||||||
expect(userService.getUserByPersonalAccessToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
accountService.getAccountByPersonalAccessToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
expect(cb).toHaveBeenCalled();
|
expect(cb).toHaveBeenCalled();
|
||||||
expect(req.header).toHaveBeenCalled();
|
expect(req.header).toHaveBeenCalled();
|
||||||
expect(req.user).toBeFalsy();
|
expect(req.user).toBeFalsy();
|
||||||
@ -61,11 +63,11 @@ test('should add user if known token', async () => {
|
|||||||
id: 44,
|
id: 44,
|
||||||
username: 'my-user',
|
username: 'my-user',
|
||||||
});
|
});
|
||||||
const userService = {
|
const accountService = {
|
||||||
getUserByPersonalAccessToken: jest.fn().mockReturnValue(apiUser),
|
getAccountByPersonalAccessToken: jest.fn().mockReturnValue(apiUser),
|
||||||
};
|
};
|
||||||
|
|
||||||
const func = patMiddleware(config, { userService });
|
const func = patMiddleware(config, { accountService });
|
||||||
|
|
||||||
const cb = jest.fn();
|
const cb = jest.fn();
|
||||||
|
|
||||||
@ -82,15 +84,15 @@ test('should add user if known token', async () => {
|
|||||||
expect(req.user).toBe(apiUser);
|
expect(req.user).toBe(apiUser);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call next if userService throws exception', async () => {
|
test('should call next if accountService throws exception', async () => {
|
||||||
getLogger.setMuteError(true);
|
getLogger.setMuteError(true);
|
||||||
const userService = {
|
const accountService = {
|
||||||
getUserByPersonalAccessToken: () => {
|
getAccountByPersonalAccessToken: () => {
|
||||||
throw new Error('Error occurred');
|
throw new Error('Error occurred');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const func = patMiddleware(config, { userService });
|
const func = patMiddleware(config, { accountService });
|
||||||
|
|
||||||
const cb = jest.fn();
|
const cb = jest.fn();
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { IAuthRequest } from '../routes/unleash-types';
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
const patMiddleware = (
|
const patMiddleware = (
|
||||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||||
{ userService }: any,
|
{ accountService }: any,
|
||||||
): any => {
|
): any => {
|
||||||
const logger = getLogger('/middleware/pat-middleware.ts');
|
const logger = getLogger('/middleware/pat-middleware.ts');
|
||||||
logger.debug('Enabling PAT middleware');
|
logger.debug('Enabling PAT middleware');
|
||||||
@ -13,11 +13,12 @@ const patMiddleware = (
|
|||||||
try {
|
try {
|
||||||
const apiToken = req.header('authorization');
|
const apiToken = req.header('authorization');
|
||||||
if (apiToken?.startsWith('user:')) {
|
if (apiToken?.startsWith('user:')) {
|
||||||
const user = await userService.getUserByPersonalAccessToken(
|
const user =
|
||||||
apiToken,
|
await accountService.getAccountByPersonalAccessToken(
|
||||||
);
|
apiToken,
|
||||||
|
);
|
||||||
req.user = user;
|
req.user = user;
|
||||||
userService.addPATSeen(apiToken);
|
accountService.addPATSeen(apiToken);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
@ -2,6 +2,7 @@ import { Request, Response } from 'express';
|
|||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { ADMIN, NONE } from '../../types/permissions';
|
import { ADMIN, NONE } from '../../types/permissions';
|
||||||
import UserService from '../../services/user-service';
|
import UserService from '../../services/user-service';
|
||||||
|
import { AccountService } from '../../services/account-service';
|
||||||
import { AccessService } from '../../services/access-service';
|
import { AccessService } from '../../services/access-service';
|
||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import { IUnleashConfig, IUnleashServices } from '../../types';
|
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||||
@ -44,6 +45,8 @@ export default class UserAdminController extends Controller {
|
|||||||
|
|
||||||
private userService: UserService;
|
private userService: UserService;
|
||||||
|
|
||||||
|
private accountService: AccountService;
|
||||||
|
|
||||||
private accessService: AccessService;
|
private accessService: AccessService;
|
||||||
|
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
@ -64,6 +67,7 @@ export default class UserAdminController extends Controller {
|
|||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
{
|
{
|
||||||
userService,
|
userService,
|
||||||
|
accountService,
|
||||||
accessService,
|
accessService,
|
||||||
emailService,
|
emailService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
@ -73,6 +77,7 @@ export default class UserAdminController extends Controller {
|
|||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
| 'userService'
|
| 'userService'
|
||||||
|
| 'accountService'
|
||||||
| 'accessService'
|
| 'accessService'
|
||||||
| 'emailService'
|
| 'emailService'
|
||||||
| 'resetTokenService'
|
| 'resetTokenService'
|
||||||
@ -83,6 +88,7 @@ export default class UserAdminController extends Controller {
|
|||||||
) {
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
|
this.accountService = accountService;
|
||||||
this.accessService = accessService;
|
this.accessService = accessService;
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
this.resetTokenService = resetTokenService;
|
this.resetTokenService = resetTokenService;
|
||||||
@ -262,7 +268,7 @@ export default class UserAdminController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUsers(req: Request, res: Response<UsersSchema>): Promise<void> {
|
async getUsers(req: Request, res: Response<UsersSchema>): Promise<void> {
|
||||||
const users = await this.userService.getAllUsers();
|
const users = await this.userService.getAll();
|
||||||
const rootRoles = await this.accessService.getRootRoles();
|
const rootRoles = await this.accessService.getRootRoles();
|
||||||
const inviteLinks = await this.resetTokenService.getActiveInvitations();
|
const inviteLinks = await this.resetTokenService.getActiveInvitations();
|
||||||
|
|
||||||
@ -310,7 +316,7 @@ export default class UserAdminController extends Controller {
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response<UsersGroupsBaseSchema>,
|
res: Response<UsersGroupsBaseSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let allUsers = await this.userService.getAll();
|
let allUsers = await this.accountService.getAll();
|
||||||
let users = allUsers.map((u) => {
|
let users = allUsers.map((u) => {
|
||||||
return {
|
return {
|
||||||
id: u.id,
|
id: u.id,
|
||||||
|
@ -9,9 +9,8 @@ import {
|
|||||||
IUserPermission,
|
IUserPermission,
|
||||||
IUserRole,
|
IUserRole,
|
||||||
} from '../types/stores/access-store';
|
} from '../types/stores/access-store';
|
||||||
import { IUserStore } from '../types/stores/user-store';
|
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { IUnleashStores } from '../types/stores';
|
import { IAccountStore, IUnleashStores } from '../types/stores';
|
||||||
import {
|
import {
|
||||||
IAvailablePermissions,
|
IAvailablePermissions,
|
||||||
ICustomRole,
|
ICustomRole,
|
||||||
@ -67,7 +66,7 @@ const isProjectPermission = (permission) => PROJECT_ADMIN.includes(permission);
|
|||||||
export class AccessService {
|
export class AccessService {
|
||||||
private store: IAccessStore;
|
private store: IAccessStore;
|
||||||
|
|
||||||
private userStore: IUserStore;
|
private accountStore: IAccountStore;
|
||||||
|
|
||||||
private roleStore: IRoleStore;
|
private roleStore: IRoleStore;
|
||||||
|
|
||||||
@ -80,18 +79,18 @@ export class AccessService {
|
|||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
accessStore,
|
accessStore,
|
||||||
userStore,
|
accountStore,
|
||||||
roleStore,
|
roleStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
'accessStore' | 'userStore' | 'roleStore' | 'environmentStore'
|
'accessStore' | 'accountStore' | 'roleStore' | 'environmentStore'
|
||||||
>,
|
>,
|
||||||
{ getLogger }: { getLogger: Function },
|
{ getLogger }: { getLogger: Function },
|
||||||
groupService: GroupService,
|
groupService: GroupService,
|
||||||
) {
|
) {
|
||||||
this.store = accessStore;
|
this.store = accessStore;
|
||||||
this.userStore = userStore;
|
this.accountStore = accountStore;
|
||||||
this.roleStore = roleStore;
|
this.roleStore = roleStore;
|
||||||
this.groupService = groupService;
|
this.groupService = groupService;
|
||||||
this.environmentStore = environmentStore;
|
this.environmentStore = environmentStore;
|
||||||
@ -363,7 +362,7 @@ export class AccessService {
|
|||||||
async getUsersForRole(roleId: number): Promise<IUser[]> {
|
async getUsersForRole(roleId: number): Promise<IUser[]> {
|
||||||
const userIdList = await this.store.getUserIdsForRole(roleId);
|
const userIdList = await this.store.getUserIdsForRole(roleId);
|
||||||
if (userIdList.length > 0) {
|
if (userIdList.length > 0) {
|
||||||
return this.userStore.getAllWithId(userIdList);
|
return this.accountStore.getAllWithId(userIdList);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -378,7 +377,7 @@ export class AccessService {
|
|||||||
);
|
);
|
||||||
if (userRoleList.length > 0) {
|
if (userRoleList.length > 0) {
|
||||||
const userIdList = userRoleList.map((u) => u.userId);
|
const userIdList = userRoleList.map((u) => u.userId);
|
||||||
const users = await this.userStore.getAllWithId(userIdList);
|
const users = await this.accountStore.getAllWithId(userIdList);
|
||||||
return users.map((user) => {
|
return users.map((user) => {
|
||||||
const role = userRoleList.find((r) => r.userId == user.id);
|
const role = userRoleList.find((r) => r.userId == user.id);
|
||||||
return {
|
return {
|
||||||
|
76
src/lib/services/account-service.ts
Normal file
76
src/lib/services/account-service.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Logger } from '../logger';
|
||||||
|
import { IUser } from '../types/user';
|
||||||
|
import { IUnleashConfig } from '../types/option';
|
||||||
|
import { IAccountStore, IUnleashStores } from '../types/stores';
|
||||||
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
|
import { AccessService } from './access-service';
|
||||||
|
import { RoleName } from '../types/model';
|
||||||
|
|
||||||
|
interface IUserWithRole extends IUser {
|
||||||
|
rootRole: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AccountService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private store: IAccountStore;
|
||||||
|
|
||||||
|
private accessService: AccessService;
|
||||||
|
|
||||||
|
private seenTimer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
private lastSeenSecrets: Set<string> = new Set<string>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
stores: Pick<IUnleashStores, 'accountStore' | 'eventStore'>,
|
||||||
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||||
|
services: {
|
||||||
|
accessService: AccessService;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
this.logger = getLogger('service/account-service.ts');
|
||||||
|
this.store = stores.accountStore;
|
||||||
|
this.accessService = services.accessService;
|
||||||
|
this.updateLastSeen();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<IUserWithRole[]> {
|
||||||
|
const accounts = await this.store.getAll();
|
||||||
|
const defaultRole = await this.accessService.getRootRole(
|
||||||
|
RoleName.VIEWER,
|
||||||
|
);
|
||||||
|
const userRoles = await this.accessService.getRootRoleForAllUsers();
|
||||||
|
const accountsWithRootRole = accounts.map((u) => {
|
||||||
|
const rootRole = userRoles.find((r) => r.userId === u.id);
|
||||||
|
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
|
||||||
|
return { ...u, rootRole: roleId };
|
||||||
|
});
|
||||||
|
return accountsWithRootRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccountByPersonalAccessToken(secret: string): Promise<IUser> {
|
||||||
|
return this.store.getAccountByPersonalAccessToken(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateLastSeen(): Promise<void> {
|
||||||
|
if (this.lastSeenSecrets.size > 0) {
|
||||||
|
const toStore = [...this.lastSeenSecrets];
|
||||||
|
this.lastSeenSecrets = new Set<string>();
|
||||||
|
await this.store.markSeenAt(toStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.seenTimer = setTimeout(
|
||||||
|
async () => this.updateLastSeen(),
|
||||||
|
minutesToMilliseconds(3),
|
||||||
|
).unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
addPATSeen(secret: string): void {
|
||||||
|
this.lastSeenSecrets.add(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
clearTimeout(this.seenTimer);
|
||||||
|
this.seenTimer = null;
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ import BadDataError from '../error/bad-data-error';
|
|||||||
import { GROUP_CREATED, GROUP_UPDATED } from '../types/events';
|
import { GROUP_CREATED, GROUP_UPDATED } from '../types/events';
|
||||||
import { IEventStore } from '../types/stores/event-store';
|
import { IEventStore } from '../types/stores/event-store';
|
||||||
import NameExistsError from '../error/name-exists-error';
|
import NameExistsError from '../error/name-exists-error';
|
||||||
import { IUserStore } from '../types/stores/user-store';
|
import { IAccountStore } from '../types/stores/account-store';
|
||||||
import { IUser } from '../types/user';
|
import { IUser } from '../types/user';
|
||||||
|
|
||||||
export class GroupService {
|
export class GroupService {
|
||||||
@ -21,18 +21,21 @@ export class GroupService {
|
|||||||
|
|
||||||
private eventStore: IEventStore;
|
private eventStore: IEventStore;
|
||||||
|
|
||||||
private userStore: IUserStore;
|
private accountStore: IAccountStore;
|
||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stores: Pick<IUnleashStores, 'groupStore' | 'eventStore' | 'userStore'>,
|
stores: Pick<
|
||||||
|
IUnleashStores,
|
||||||
|
'groupStore' | 'eventStore' | 'accountStore'
|
||||||
|
>,
|
||||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||||
) {
|
) {
|
||||||
this.logger = getLogger('service/group-service.js');
|
this.logger = getLogger('service/group-service.js');
|
||||||
this.groupStore = stores.groupStore;
|
this.groupStore = stores.groupStore;
|
||||||
this.eventStore = stores.eventStore;
|
this.eventStore = stores.eventStore;
|
||||||
this.userStore = stores.userStore;
|
this.accountStore = stores.accountStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<IGroupModel[]> {
|
async getAll(): Promise<IGroupModel[]> {
|
||||||
@ -40,7 +43,7 @@ export class GroupService {
|
|||||||
const allGroupUsers = await this.groupStore.getAllUsersByGroups(
|
const allGroupUsers = await this.groupStore.getAllUsersByGroups(
|
||||||
groups.map((g) => g.id),
|
groups.map((g) => g.id),
|
||||||
);
|
);
|
||||||
const users = await this.userStore.getAllWithId(
|
const users = await this.accountStore.getAllWithId(
|
||||||
allGroupUsers.map((u) => u.userId),
|
allGroupUsers.map((u) => u.userId),
|
||||||
);
|
);
|
||||||
const groupProjects = await this.groupStore.getGroupProjects(
|
const groupProjects = await this.groupStore.getGroupProjects(
|
||||||
@ -72,7 +75,7 @@ export class GroupService {
|
|||||||
async getGroup(id: number): Promise<IGroupModel> {
|
async getGroup(id: number): Promise<IGroupModel> {
|
||||||
const group = await this.groupStore.get(id);
|
const group = await this.groupStore.get(id);
|
||||||
const groupUsers = await this.groupStore.getAllUsersByGroups([id]);
|
const groupUsers = await this.groupStore.getAllUsersByGroups([id]);
|
||||||
const users = await this.userStore.getAllWithId(
|
const users = await this.accountStore.getAllWithId(
|
||||||
groupUsers.map((u) => u.userId),
|
groupUsers.map((u) => u.userId),
|
||||||
);
|
);
|
||||||
return this.mapGroupWithUsers(group, groupUsers, users);
|
return this.mapGroupWithUsers(group, groupUsers, users);
|
||||||
@ -156,7 +159,7 @@ export class GroupService {
|
|||||||
groups.map((g) => g.id),
|
groups.map((g) => g.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const users = await this.userStore.getAllWithId(
|
const users = await this.accountStore.getAllWithId(
|
||||||
groupUsers.map((u) => u.userId),
|
groupUsers.map((u) => u.userId),
|
||||||
);
|
);
|
||||||
return groups.map((group) => {
|
return groups.map((group) => {
|
||||||
|
@ -42,6 +42,7 @@ import MaintenanceService from './maintenance-service';
|
|||||||
import ExportImportService from './export-import-service';
|
import ExportImportService from './export-import-service';
|
||||||
import SchedulerService from './scheduler-service';
|
import SchedulerService from './scheduler-service';
|
||||||
import { minutesToMilliseconds } from 'date-fns';
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
|
import { AccountService } from './account-service';
|
||||||
|
|
||||||
export const createServices = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -76,6 +77,9 @@ export const createServices = (
|
|||||||
sessionService,
|
sessionService,
|
||||||
settingService,
|
settingService,
|
||||||
});
|
});
|
||||||
|
const accountService = new AccountService(stores, config, {
|
||||||
|
accessService,
|
||||||
|
});
|
||||||
const versionService = new VersionService(stores, config);
|
const versionService = new VersionService(stores, config);
|
||||||
const healthService = new HealthService(stores, config);
|
const healthService = new HealthService(stores, config);
|
||||||
const userFeedbackService = new UserFeedbackService(stores, config);
|
const userFeedbackService = new UserFeedbackService(stores, config);
|
||||||
@ -159,6 +163,7 @@ export const createServices = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accessService,
|
accessService,
|
||||||
|
accountService,
|
||||||
addonService,
|
addonService,
|
||||||
featureToggleService: featureToggleServiceV2,
|
featureToggleService: featureToggleServiceV2,
|
||||||
featureToggleServiceV2,
|
featureToggleServiceV2,
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
ProjectGroupRemovedEvent,
|
ProjectGroupRemovedEvent,
|
||||||
ProjectGroupUpdateRoleEvent,
|
ProjectGroupUpdateRoleEvent,
|
||||||
} from '../types/events';
|
} from '../types/events';
|
||||||
import { IUnleashStores, IUnleashConfig } from '../types';
|
import { IUnleashStores, IUnleashConfig, IAccountStore } from '../types';
|
||||||
import {
|
import {
|
||||||
FeatureToggle,
|
FeatureToggle,
|
||||||
IProject,
|
IProject,
|
||||||
@ -42,7 +42,6 @@ import IncompatibleProjectError from '../error/incompatible-project-error';
|
|||||||
import { DEFAULT_PROJECT } from '../types/project';
|
import { DEFAULT_PROJECT } from '../types/project';
|
||||||
import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store';
|
import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store';
|
||||||
import ProjectWithoutOwnerError from '../error/project-without-owner-error';
|
import ProjectWithoutOwnerError from '../error/project-without-owner-error';
|
||||||
import { IUserStore } from 'lib/types/stores/user-store';
|
|
||||||
import { arraysHaveSameItems } from '../util/arraysHaveSameItems';
|
import { arraysHaveSameItems } from '../util/arraysHaveSameItems';
|
||||||
import { GroupService } from './group-service';
|
import { GroupService } from './group-service';
|
||||||
import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group';
|
import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group';
|
||||||
@ -79,7 +78,7 @@ export default class ProjectService {
|
|||||||
|
|
||||||
private tagStore: IFeatureTagStore;
|
private tagStore: IFeatureTagStore;
|
||||||
|
|
||||||
private userStore: IUserStore;
|
private accountStore: IAccountStore;
|
||||||
|
|
||||||
private favoritesService: FavoritesService;
|
private favoritesService: FavoritesService;
|
||||||
|
|
||||||
@ -92,7 +91,7 @@ export default class ProjectService {
|
|||||||
environmentStore,
|
environmentStore,
|
||||||
featureEnvironmentStore,
|
featureEnvironmentStore,
|
||||||
featureTagStore,
|
featureTagStore,
|
||||||
userStore,
|
accountStore,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
| 'projectStore'
|
| 'projectStore'
|
||||||
@ -102,7 +101,7 @@ export default class ProjectService {
|
|||||||
| 'environmentStore'
|
| 'environmentStore'
|
||||||
| 'featureEnvironmentStore'
|
| 'featureEnvironmentStore'
|
||||||
| 'featureTagStore'
|
| 'featureTagStore'
|
||||||
| 'userStore'
|
| 'accountStore'
|
||||||
>,
|
>,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
accessService: AccessService,
|
accessService: AccessService,
|
||||||
@ -120,7 +119,7 @@ export default class ProjectService {
|
|||||||
this.featureToggleService = featureToggleService;
|
this.featureToggleService = featureToggleService;
|
||||||
this.favoritesService = favoriteService;
|
this.favoritesService = favoriteService;
|
||||||
this.tagStore = featureTagStore;
|
this.tagStore = featureTagStore;
|
||||||
this.userStore = userStore;
|
this.accountStore = accountStore;
|
||||||
this.groupService = groupService;
|
this.groupService = groupService;
|
||||||
this.logger = config.getLogger('services/project-service.js');
|
this.logger = config.getLogger('services/project-service.js');
|
||||||
}
|
}
|
||||||
@ -317,7 +316,7 @@ export default class ProjectService {
|
|||||||
const [roles, users] = await this.accessService.getProjectRoleAccess(
|
const [roles, users] = await this.accessService.getProjectRoleAccess(
|
||||||
projectId,
|
projectId,
|
||||||
);
|
);
|
||||||
const user = await this.userStore.get(userId);
|
const user = await this.accountStore.get(userId);
|
||||||
|
|
||||||
const role = roles.find((r) => r.id === roleId);
|
const role = roles.find((r) => r.id === roleId);
|
||||||
if (!role) {
|
if (!role) {
|
||||||
@ -359,7 +358,7 @@ export default class ProjectService {
|
|||||||
|
|
||||||
await this.accessService.removeUserFromRole(userId, role.id, projectId);
|
await this.accessService.removeUserFromRole(userId, role.id, projectId);
|
||||||
|
|
||||||
const user = await this.userStore.get(userId);
|
const user = await this.accountStore.get(userId);
|
||||||
|
|
||||||
await this.eventStore.store(
|
await this.eventStore.store(
|
||||||
new ProjectUserRemovedEvent({
|
new ProjectUserRemovedEvent({
|
||||||
|
@ -28,7 +28,6 @@ import PasswordMismatch from '../error/password-mismatch';
|
|||||||
import BadDataError from '../error/bad-data-error';
|
import BadDataError from '../error/bad-data-error';
|
||||||
import { isDefined } from '../util/isDefined';
|
import { isDefined } from '../util/isDefined';
|
||||||
import { TokenUserSchema } from '../openapi/spec/token-user-schema';
|
import { TokenUserSchema } from '../openapi/spec/token-user-schema';
|
||||||
import { minutesToMilliseconds } from 'date-fns';
|
|
||||||
|
|
||||||
const systemUser = new User({ id: -1, username: 'system' });
|
const systemUser = new User({ id: -1, username: 'system' });
|
||||||
|
|
||||||
@ -79,10 +78,6 @@ class UserService {
|
|||||||
|
|
||||||
private passwordResetTimeouts: { [key: string]: NodeJS.Timeout } = {};
|
private passwordResetTimeouts: { [key: string]: NodeJS.Timeout } = {};
|
||||||
|
|
||||||
private seenTimer: NodeJS.Timeout;
|
|
||||||
|
|
||||||
private lastSeenSecrets: Set<string> = new Set<string>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stores: Pick<IUnleashStores, 'userStore' | 'eventStore'>,
|
stores: Pick<IUnleashStores, 'userStore' | 'eventStore'>,
|
||||||
{
|
{
|
||||||
@ -108,7 +103,6 @@ class UserService {
|
|||||||
if (authentication && authentication.createAdminUser) {
|
if (authentication && authentication.createAdminUser) {
|
||||||
process.nextTick(() => this.initAdminUser());
|
process.nextTick(() => this.initAdminUser());
|
||||||
}
|
}
|
||||||
this.updateLastSeen();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePassword(password: string): boolean {
|
validatePassword(password: string): boolean {
|
||||||
@ -161,20 +155,6 @@ class UserService {
|
|||||||
return usersWithRootRole;
|
return usersWithRootRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllUsers(): Promise<IUserWithRole[]> {
|
|
||||||
const users = await this.store.getAllUsers();
|
|
||||||
const defaultRole = await this.accessService.getRootRole(
|
|
||||||
RoleName.VIEWER,
|
|
||||||
);
|
|
||||||
const userRoles = await this.accessService.getRootRoleForAllUsers();
|
|
||||||
const usersWithRootRole = users.map((u) => {
|
|
||||||
const rootRole = userRoles.find((r) => r.userId === u.id);
|
|
||||||
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
|
|
||||||
return { ...u, rootRole: roleId };
|
|
||||||
});
|
|
||||||
return usersWithRootRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUser(id: number): Promise<IUserWithRole> {
|
async getUser(id: number): Promise<IUserWithRole> {
|
||||||
const roles = await this.accessService.getUserRootRoles(id);
|
const roles = await this.accessService.getUserRootRoles(id);
|
||||||
const defaultRole = await this.accessService.getRootRole(
|
const defaultRole = await this.accessService.getRootRole(
|
||||||
@ -442,32 +422,6 @@ class UserService {
|
|||||||
);
|
);
|
||||||
return resetLink;
|
return resetLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByPersonalAccessToken(secret: string): Promise<IUser> {
|
|
||||||
return this.store.getUserByPersonalAccessToken(secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateLastSeen(): Promise<void> {
|
|
||||||
if (this.lastSeenSecrets.size > 0) {
|
|
||||||
const toStore = [...this.lastSeenSecrets];
|
|
||||||
this.lastSeenSecrets = new Set<string>();
|
|
||||||
await this.store.markSeenAt(toStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.seenTimer = setTimeout(
|
|
||||||
async () => this.updateLastSeen(),
|
|
||||||
minutesToMilliseconds(3),
|
|
||||||
).unref();
|
|
||||||
}
|
|
||||||
|
|
||||||
addPATSeen(secret: string): void {
|
|
||||||
this.lastSeenSecrets.add(secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
|
||||||
clearTimeout(this.seenTimer);
|
|
||||||
this.seenTimer = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = UserService;
|
module.exports = UserService;
|
||||||
|
@ -38,9 +38,11 @@ import { InstanceStatsService } from '../services/instance-stats-service';
|
|||||||
import { FavoritesService } from '../services';
|
import { FavoritesService } from '../services';
|
||||||
import MaintenanceService from '../services/maintenance-service';
|
import MaintenanceService from '../services/maintenance-service';
|
||||||
import ExportImportService from 'lib/services/export-import-service';
|
import ExportImportService from 'lib/services/export-import-service';
|
||||||
|
import { AccountService } from '../services/account-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
|
accountService: AccountService;
|
||||||
addonService: AddonService;
|
addonService: AddonService;
|
||||||
apiTokenService: ApiTokenService;
|
apiTokenService: ApiTokenService;
|
||||||
clientInstanceService: ClientInstanceService;
|
clientInstanceService: ClientInstanceService;
|
||||||
|
@ -30,9 +30,11 @@ import { IPatStore } from './stores/pat-store';
|
|||||||
import { IPublicSignupTokenStore } from './stores/public-signup-token-store';
|
import { IPublicSignupTokenStore } from './stores/public-signup-token-store';
|
||||||
import { IFavoriteFeaturesStore } from './stores/favorite-features';
|
import { IFavoriteFeaturesStore } from './stores/favorite-features';
|
||||||
import { IFavoriteProjectsStore } from './stores/favorite-projects';
|
import { IFavoriteProjectsStore } from './stores/favorite-projects';
|
||||||
|
import { IAccountStore } from './stores/account-store';
|
||||||
|
|
||||||
export interface IUnleashStores {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
|
accountStore: IAccountStore;
|
||||||
addonStore: IAddonStore;
|
addonStore: IAddonStore;
|
||||||
apiTokenStore: IApiTokenStore;
|
apiTokenStore: IApiTokenStore;
|
||||||
clientApplicationsStore: IClientApplicationsStore;
|
clientApplicationsStore: IClientApplicationsStore;
|
||||||
@ -68,6 +70,7 @@ export interface IUnleashStores {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
IAccessStore,
|
IAccessStore,
|
||||||
|
IAccountStore,
|
||||||
IAddonStore,
|
IAddonStore,
|
||||||
IApiTokenStore,
|
IApiTokenStore,
|
||||||
IClientApplicationsStore,
|
IClientApplicationsStore,
|
||||||
|
18
src/lib/types/stores/account-store.ts
Normal file
18
src/lib/types/stores/account-store.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { IUser } from '../user';
|
||||||
|
import { Store } from './store';
|
||||||
|
|
||||||
|
export interface IUserLookup {
|
||||||
|
id?: number;
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountStore extends Store<IUser, number> {
|
||||||
|
hasAccount(idQuery: IUserLookup): Promise<number | undefined>;
|
||||||
|
search(query: string): Promise<IUser[]>;
|
||||||
|
getAllWithId(userIdList: number[]): Promise<IUser[]>;
|
||||||
|
getByQuery(idQuery: IUserLookup): Promise<IUser>;
|
||||||
|
count(): Promise<number>;
|
||||||
|
getAccountByPersonalAccessToken(secret: string): Promise<IUser>;
|
||||||
|
markSeenAt(secrets: string[]): Promise<void>;
|
||||||
|
}
|
@ -25,7 +25,6 @@ export interface IUserStore extends Store<IUser, number> {
|
|||||||
upsert(user: ICreateUser): Promise<IUser>;
|
upsert(user: ICreateUser): Promise<IUser>;
|
||||||
hasUser(idQuery: IUserLookup): Promise<number | undefined>;
|
hasUser(idQuery: IUserLookup): Promise<number | undefined>;
|
||||||
search(query: string): Promise<IUser[]>;
|
search(query: string): Promise<IUser[]>;
|
||||||
getAllUsers(): Promise<IUser[]>;
|
|
||||||
getAllWithId(userIdList: number[]): Promise<IUser[]>;
|
getAllWithId(userIdList: number[]): Promise<IUser[]>;
|
||||||
getByQuery(idQuery: IUserLookup): Promise<IUser>;
|
getByQuery(idQuery: IUserLookup): Promise<IUser>;
|
||||||
getPasswordHash(userId: number): Promise<string>;
|
getPasswordHash(userId: number): Promise<string>;
|
||||||
@ -33,6 +32,4 @@ export interface IUserStore extends Store<IUser, number> {
|
|||||||
incLoginAttempts(user: IUser): Promise<void>;
|
incLoginAttempts(user: IUser): Promise<void>;
|
||||||
successfullyLogin(user: IUser): Promise<void>;
|
successfullyLogin(user: IUser): Promise<void>;
|
||||||
count(): Promise<number>;
|
count(): Promise<number>;
|
||||||
getUserByPersonalAccessToken(secret: string): Promise<IUser>;
|
|
||||||
markSeenAt(secrets: string[]): Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
2
src/test/fixtures/access-service-mock.ts
vendored
2
src/test/fixtures/access-service-mock.ts
vendored
@ -16,7 +16,7 @@ class AccessServiceMock extends AccessService {
|
|||||||
super(
|
super(
|
||||||
{
|
{
|
||||||
accessStore: undefined,
|
accessStore: undefined,
|
||||||
userStore: undefined,
|
accountStore: undefined,
|
||||||
roleStore: undefined,
|
roleStore: undefined,
|
||||||
environmentStore: undefined,
|
environmentStore: undefined,
|
||||||
},
|
},
|
||||||
|
96
src/test/fixtures/fake-account-store.ts
vendored
Normal file
96
src/test/fixtures/fake-account-store.ts
vendored
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { IUser } from '../../lib/types/user';
|
||||||
|
import {
|
||||||
|
// ICreateUser,
|
||||||
|
IUserLookup,
|
||||||
|
IAccountStore,
|
||||||
|
} from '../../lib/types/stores/account-store';
|
||||||
|
|
||||||
|
export class FakeAccountStore implements IAccountStore {
|
||||||
|
data: IUser[];
|
||||||
|
|
||||||
|
idSeq: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.idSeq = 1;
|
||||||
|
this.data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasAccount({
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
}: IUserLookup): Promise<number | undefined> {
|
||||||
|
const user = this.data.find((i) => {
|
||||||
|
if (id && i.id === id) return true;
|
||||||
|
if (username && i.username === username) return true;
|
||||||
|
if (email && i.email === email) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (user) {
|
||||||
|
return user.id;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {}
|
||||||
|
|
||||||
|
async exists(key: number): Promise<boolean> {
|
||||||
|
return this.data.some((u) => u.id === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async count(): Promise<number> {
|
||||||
|
return this.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: number): Promise<IUser> {
|
||||||
|
return this.data.find((u) => u.id === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByQuery({ id, username, email }: IUserLookup): Promise<IUser> {
|
||||||
|
const user = this.data.find((i) => {
|
||||||
|
if (i.id && i.id === id) return true;
|
||||||
|
if (i.username && i.username === username) return true;
|
||||||
|
if (i.email && i.email === email) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (user) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
throw new Error('Could not find user');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<IUser[]> {
|
||||||
|
return Promise.resolve(this.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<void> {
|
||||||
|
this.data = this.data.filter((item) => item.id !== id);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSelectUser(): any {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(): Promise<IUser[]> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllWithId(): Promise<IUser[]> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAll(): Promise<void> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
getAccountByPersonalAccessToken(secret: string): Promise<IUser> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async markSeenAt(secrets: string[]): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
}
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -31,6 +31,7 @@ import FakePatStore from './fake-pat-store';
|
|||||||
import FakePublicSignupStore from './fake-public-signup-store';
|
import FakePublicSignupStore from './fake-public-signup-store';
|
||||||
import FakeFavoriteFeaturesStore from './fake-favorite-features-store';
|
import FakeFavoriteFeaturesStore from './fake-favorite-features-store';
|
||||||
import FakeFavoriteProjectsStore from './fake-favorite-projects-store';
|
import FakeFavoriteProjectsStore from './fake-favorite-projects-store';
|
||||||
|
import { FakeAccountStore } from './fake-account-store';
|
||||||
|
|
||||||
const createStores: () => IUnleashStores = () => {
|
const createStores: () => IUnleashStores = () => {
|
||||||
const db = {
|
const db = {
|
||||||
@ -56,6 +57,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
projectStore: new FakeProjectStore(),
|
projectStore: new FakeProjectStore(),
|
||||||
userStore: new FakeUserStore(),
|
userStore: new FakeUserStore(),
|
||||||
accessStore: new FakeAccessStore(),
|
accessStore: new FakeAccessStore(),
|
||||||
|
accountStore: new FakeAccountStore(),
|
||||||
userFeedbackStore: new FakeUserFeedbackStore(),
|
userFeedbackStore: new FakeUserFeedbackStore(),
|
||||||
featureStrategiesStore: new FakeFeatureStrategiesStore(),
|
featureStrategiesStore: new FakeFeatureStrategiesStore(),
|
||||||
featureTagStore: new FakeFeatureTagStore(),
|
featureTagStore: new FakeFeatureTagStore(),
|
||||||
|
Loading…
Reference in New Issue
Block a user