Skip to content

Commit

Permalink
feat: add Unleash admins to API payload (#8299)
Browse files Browse the repository at this point in the history
Adds Unleash admins to the personal dashboard payload.

Uses the access store (and a new method) to fetch admins and maps it to
a new `MinimalUser` type. We already have a `User` class, but it
contains a lot of information we don't care about here, such as `isAPI`,
SCIM data etc.

In the UI, admins will be shown to users who are not part of any
projects. This is the default state for new viewer users, and can also
happen for editors if you archive the default project, for instance.

Tests in a follow-up PR
  • Loading branch information
thomasheartman authored Sep 30, 2024
1 parent 751c2fa commit a4ea46d
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 6 deletions.
32 changes: 31 additions & 1 deletion src/lib/db/account-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import User from '../types/user';
import NotFoundError from '../error/notfound-error';
import type { IUserLookup } from '../types/stores/user-store';
import type { IAdminCount } from '../types/stores/account-store';
import type { IAccountStore } from '../types';
import type { IAccountStore, MinimalUser } from '../types';
import type { Db } from './db';

const TABLE = 'users';
Expand Down Expand Up @@ -198,4 +198,34 @@ export class AccountStore implements IAccountStore {
service: adminCount[0].service,
};
}

async getAdmins(): Promise<MinimalUser[]> {
const rowToAdminUser = (row) => {
return {
id: row.id,
name: emptify(row.name),
username: emptify(row.username),
email: emptify(row.email),
imageUrl: emptify(row.image_url),
};
};

const admins = 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']),
)
.andWhereNot('users.is_service', true)
.select(
'users.id',
'users.name',
'users.username',
'users.email',
'users.image_url',
);

return admins.map(rowToAdminUser);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { FeatureEventFormatterMd } from '../../addons/feature-event-formatter-md
import FakeEventStore from '../../../test/fixtures/fake-event-store';
import { FakePrivateProjectChecker } from '../private-project/fakePrivateProjectChecker';
import { PrivateProjectChecker } from '../private-project/privateProjectChecker';
import { AccountStore } from '../../db/account-store';
import { FakeAccountStore } from '../../../test/fixtures/fake-account-store';

export const createPersonalDashboardService = (
db: Db,
Expand All @@ -28,6 +30,7 @@ export const createPersonalDashboardService = (
formatStyle: 'markdown',
}),
new PrivateProjectChecker(stores, config),
new AccountStore(db, config.getLogger),
);
};

Expand All @@ -42,5 +45,6 @@ export const createFakePersonalDashboardService = (config: IUnleashConfig) => {
formatStyle: 'markdown',
}),
new FakePrivateProjectChecker(),
new FakeAccountStore(),
);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { IUser } from '../../server-impl';
import type {
BasePersonalProject,
IPersonalDashboardReadModel,
Expand All @@ -14,4 +15,8 @@ export class FakePersonalDashboardReadModel
async getPersonalProjects(userId: number): Promise<BasePersonalProject[]> {
return [];
}

async getAdmins(): Promise<IUser[]> {
return [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,18 @@ export default class PersonalDashboardController extends Controller {
): Promise<void> {
const user = req.user;

const [flags, projects, projectOwners] = await Promise.all([
const [flags, projects, projectOwners, admins] = await Promise.all([
this.personalDashboardService.getPersonalFeatures(user.id),
this.personalDashboardService.getPersonalProjects(user.id),
this.personalDashboardService.getProjectOwners(user.id),
this.personalDashboardService.getAdmins(),
]);

this.openApiService.respondWithValidation(
200,
res,
personalDashboardSchema.$id,
{ projects, flags, projectOwners },
{ projects, flags, projectOwners, admins },
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
} from './personal-dashboard-read-model-type';
import type { IProjectReadModel } from '../project/project-read-model-type';
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
import type { IEventStore } from '../../types';
import type { IAccountStore, IEventStore, MinimalUser } from '../../types';
import type { FeatureEventFormatter } from '../../addons/feature-event-formatter-md';
import { generateImageUrl } from '../../util';

Expand All @@ -35,20 +35,24 @@ export class PersonalDashboardService {

private featureEventFormatter: FeatureEventFormatter;

private accountStore: IAccountStore;

constructor(
personalDashboardReadModel: IPersonalDashboardReadModel,
projectOwnersReadModel: IProjectOwnersReadModel,
projectReadModel: IProjectReadModel,
eventStore: IEventStore,
featureEventFormatter: FeatureEventFormatter,
privateProjectChecker: IPrivateProjectChecker,
accountStore: IAccountStore,
) {
this.personalDashboardReadModel = personalDashboardReadModel;
this.projectOwnersReadModel = projectOwnersReadModel;
this.projectReadModel = projectReadModel;
this.eventStore = eventStore;
this.featureEventFormatter = featureEventFormatter;
this.privateProjectChecker = privateProjectChecker;
this.accountStore = accountStore;
}

getPersonalFeatures(userId: number): Promise<PersonalFeature[]> {
Expand Down Expand Up @@ -105,4 +109,8 @@ export class PersonalDashboardService {

return { latestEvents: formattedEvents };
}

async getAdmins(): Promise<MinimalUser[]> {
return this.accountStore.getAdmins();
}
}
41 changes: 41 additions & 0 deletions src/lib/openapi/spec/personal-dashboard-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,41 @@ export const personalDashboardSchema = {
additionalProperties: false,
required: ['projects', 'flags'],
properties: {
admins: {
type: 'array',
description: 'Users with the admin role in Unleash.',
items: {
type: 'object',
required: ['id'],
properties: {
id: {
type: 'integer',
description: 'The user ID.',
example: 1,
},
name: {
type: 'string',
description: "The user's name.",
example: 'Ash Ketchum',
},
username: {
type: 'string',
description: "The user's username.",
example: 'pokémaster13',
},
imageUrl: {
type: 'string',
nullable: true,
example: 'https://example.com/peek-at-you.jpg',
},
email: {
type: 'string',
nullable: true,
example: '[email protected]',
},
},
},
},
projectOwners: {
type: 'array',
description:
Expand All @@ -18,19 +53,25 @@ export const personalDashboardSchema = {
ownerType: {
type: 'string',
enum: ['user'],
description:
'The type of the owner; will always be `user`.',
},
name: {
type: 'string',
example: 'User Name',
description:
"The name displayed for the user. Can be the user's name, username, or email, depending on what they have provided.",
},
imageUrl: {
type: 'string',
nullable: true,
description: "The URL of the user's profile image.",
example: 'https://example.com/image.jpg',
},
email: {
type: 'string',
nullable: true,
description: "The user's email address.",
example: '[email protected]',
},
},
Expand Down
3 changes: 2 additions & 1 deletion src/lib/types/stores/account-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IUser } from '../user';
import type { IUser, MinimalUser } from '../user';
import type { Store } from './store';

export interface IUserLookup {
Expand All @@ -22,4 +22,5 @@ export interface IAccountStore extends Store<IUser, number> {
getAccountByPersonalAccessToken(secret: string): Promise<IUser>;
markSeenAt(secrets: string[]): Promise<void>;
getAdminCount(): Promise<IAdminCount>;
getAdmins(): Promise<MinimalUser[]>;
}
5 changes: 5 additions & 0 deletions src/lib/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export interface IUser {
scimId?: string;
}

export type MinimalUser = Pick<
IUser,
'id' | 'name' | 'username' | 'email' | 'imageUrl'
>;

export interface IProjectUser extends IUser {
addedAt: Date;
}
Expand Down
5 changes: 4 additions & 1 deletion src/test/fixtures/fake-account-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export class FakeAccountStore implements IAccountStore {
this.idSeq = 1;
this.data = [];
}

async hasAccount({
id,
username,
Expand Down Expand Up @@ -98,4 +97,8 @@ export class FakeAccountStore implements IAccountStore {
async getAdminCount(): Promise<IAdminCount> {
throw new Error('Not implemented');
}

async getAdmins(): Promise<IUser[]> {
return [];
}
}

0 comments on commit a4ea46d

Please sign in to comment.