1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: add Unleash admins to API payload (#8299)

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
This commit is contained in:
Thomas Heartman 2024-09-30 13:01:30 +02:00 committed by GitHub
parent 751c2fa902
commit a4ea46dab6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 104 additions and 6 deletions

View File

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

View File

@ -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,
@ -28,6 +30,7 @@ export const createPersonalDashboardService = (
formatStyle: 'markdown',
}),
new PrivateProjectChecker(stores, config),
new AccountStore(db, config.getLogger),
);
};
@ -42,5 +45,6 @@ export const createFakePersonalDashboardService = (config: IUnleashConfig) => {
formatStyle: 'markdown',
}),
new FakePrivateProjectChecker(),
new FakeAccountStore(),
);
};

View File

@ -1,3 +1,4 @@
import type { IUser } from '../../server-impl';
import type {
BasePersonalProject,
IPersonalDashboardReadModel,
@ -14,4 +15,8 @@ export class FakePersonalDashboardReadModel
async getPersonalProjects(userId: number): Promise<BasePersonalProject[]> {
return [];
}
async getAdmins(): Promise<IUser[]> {
return [];
}
}

View File

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

View File

@ -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';
@ -35,6 +35,8 @@ export class PersonalDashboardService {
private featureEventFormatter: FeatureEventFormatter;
private accountStore: IAccountStore;
constructor(
personalDashboardReadModel: IPersonalDashboardReadModel,
projectOwnersReadModel: IProjectOwnersReadModel,
@ -42,6 +44,7 @@ export class PersonalDashboardService {
eventStore: IEventStore,
featureEventFormatter: FeatureEventFormatter,
privateProjectChecker: IPrivateProjectChecker,
accountStore: IAccountStore,
) {
this.personalDashboardReadModel = personalDashboardReadModel;
this.projectOwnersReadModel = projectOwnersReadModel;
@ -49,6 +52,7 @@ export class PersonalDashboardService {
this.eventStore = eventStore;
this.featureEventFormatter = featureEventFormatter;
this.privateProjectChecker = privateProjectChecker;
this.accountStore = accountStore;
}
getPersonalFeatures(userId: number): Promise<PersonalFeature[]> {
@ -105,4 +109,8 @@ export class PersonalDashboardService {
return { latestEvents: formattedEvents };
}
async getAdmins(): Promise<MinimalUser[]> {
return this.accountStore.getAdmins();
}
}

View File

@ -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: 'user@example.com',
},
},
},
},
projectOwners: {
type: 'array',
description:
@ -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: 'user@example.com',
},
},

View File

@ -1,4 +1,4 @@
import type { IUser } from '../user';
import type { IUser, MinimalUser } from '../user';
import type { Store } from './store';
export interface IUserLookup {
@ -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[]>;
}

View File

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

View File

@ -15,7 +15,6 @@ export class FakeAccountStore implements IAccountStore {
this.idSeq = 1;
this.data = [];
}
async hasAccount({
id,
username,
@ -98,4 +97,8 @@ export class FakeAccountStore implements IAccountStore {
async getAdminCount(): Promise<IAdminCount> {
throw new Error('Not implemented');
}
async getAdmins(): Promise<IUser[]> {
return [];
}
}