fix(admin): organize admin panel (#7840)

This commit is contained in:
forehalo
2024-08-13 05:45:02 +00:00
parent 6dea831d8a
commit 0ec1995add
45 changed files with 746 additions and 955 deletions

View File

@@ -2,7 +2,10 @@ import { Module } from '@nestjs/common';
import { UserModule } from '../user';
import { EarlyAccessType, FeatureManagementService } from './management';
import { FeatureManagementResolver } from './resolver';
import {
AdminFeatureManagementResolver,
FeatureManagementResolver,
} from './resolver';
import { FeatureService } from './service';
/**
@@ -17,6 +20,7 @@ import { FeatureService } from './service';
FeatureService,
FeatureManagementService,
FeatureManagementResolver,
AdminFeatureManagementResolver,
],
exports: [FeatureService, FeatureManagementService],
})

View File

@@ -42,10 +42,6 @@ export class FeatureManagementService {
return this.feature.addUserFeature(userId, FeatureType.Admin, 'Admin user');
}
removeAdmin(userId: string) {
return this.feature.removeUserFeature(userId, FeatureType.Admin);
}
// ======== Early Access ========
async addEarlyAccess(
userId: string,

View File

@@ -1,21 +1,18 @@
import {
Args,
Context,
Int,
Mutation,
Parent,
Query,
registerEnumType,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { difference } from 'lodash-es';
import { UserNotFound } from '../../fundamentals';
import { sessionUser } from '../auth/service';
import { Config } from '../../fundamentals';
import { Admin } from '../common';
import { UserService } from '../user/service';
import { UserType } from '../user/types';
import { EarlyAccessType, FeatureManagementService } from './management';
import { FeatureService } from './service';
import { FeatureType } from './types';
registerEnumType(EarlyAccessType, {
@@ -24,10 +21,7 @@ registerEnumType(EarlyAccessType, {
@Resolver(() => UserType)
export class FeatureManagementResolver {
constructor(
private readonly users: UserService,
private readonly feature: FeatureManagementService
) {}
constructor(private readonly feature: FeatureManagementService) {}
@ResolveField(() => [FeatureType], {
name: 'features',
@@ -36,75 +30,48 @@ export class FeatureManagementResolver {
async userFeatures(@Parent() user: UserType) {
return this.feature.getActivatedUserFeatures(user.id);
}
}
@Admin()
@Mutation(() => Int)
async addToEarlyAccess(
@Args('email') email: string,
@Args({ name: 'type', type: () => EarlyAccessType }) type: EarlyAccessType
): Promise<number> {
const user = await this.users.findUserByEmail(email);
if (user) {
return this.feature.addEarlyAccess(user.id, type);
} else {
const user = await this.users.createUser({
email,
registered: false,
});
return this.feature.addEarlyAccess(user.id, type);
}
}
export class AvailableUserFeatureConfig {
constructor(private readonly config: Config) {}
@Admin()
@Mutation(() => Int)
async removeEarlyAccess(
@Args('email') email: string,
@Args({ name: 'type', type: () => EarlyAccessType }) type: EarlyAccessType
): Promise<number> {
const user = await this.users.findUserByEmail(email);
if (!user) {
throw new UserNotFound();
}
return this.feature.removeEarlyAccess(user.id, type);
}
@Admin()
@Query(() => [UserType])
async earlyAccessUsers(
@Context() ctx: { isAdminQuery: boolean }
): Promise<UserType[]> {
// allow query other user's subscription
ctx.isAdminQuery = true;
return this.feature.listEarlyAccess().then(users => {
return users.map(sessionUser);
});
}
@Admin()
@Mutation(() => Boolean)
async addAdminister(@Args('email') email: string): Promise<boolean> {
const user = await this.users.findUserByEmail(email);
if (!user) {
throw new UserNotFound();
}
await this.feature.addAdmin(user.id);
return true;
}
@Admin()
@Mutation(() => Boolean)
async removeAdminister(@Args('email') email: string): Promise<boolean> {
const user = await this.users.findUserByEmail(email);
if (!user) {
throw new UserNotFound();
}
await this.feature.removeAdmin(user.id);
return true;
async availableUserFeatures() {
return this.config.isSelfhosted
? [FeatureType.Admin]
: [FeatureType.EarlyAccess, FeatureType.AIEarlyAccess, FeatureType.Admin];
}
}
@Admin()
@Resolver(() => Boolean)
export class AdminFeatureManagementResolver extends AvailableUserFeatureConfig {
constructor(
config: Config,
private readonly feature: FeatureService
) {
super(config);
}
@Mutation(() => [FeatureType], {
description: 'update user enabled feature',
})
async updateUserFeatures(
@Args('id') id: string,
@Args({ name: 'features', type: () => [FeatureType] })
features: FeatureType[]
) {
const configurableFeatures = await this.availableUserFeatures();
const removed = difference(configurableFeatures, features);
await Promise.all(
features.map(feature =>
this.feature.addUserFeature(id, feature, 'admin panel')
)
);
await Promise.all(
removed.map(feature => this.feature.removeUserFeature(id, feature))
);
return features;
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { CannotDeleteAllAdminAccount } from '../../fundamentals';
import { WorkspaceType } from '../workspaces/types';
import { FeatureConfigType, getFeature } from './feature';
import { FeatureKind, FeatureType } from './types';
@@ -81,6 +82,9 @@ export class FeatureService {
}
async removeUserFeature(userId: string, feature: FeatureType) {
if (feature === FeatureType.Admin) {
await this.ensureNotLastAdmin(userId);
}
return this.prisma.userFeature
.updateMany({
where: {
@@ -98,6 +102,20 @@ export class FeatureService {
.then(r => r.count);
}
async ensureNotLastAdmin(userId: string) {
const count = await this.prisma.userFeature.count({
where: {
userId: { not: userId },
feature: { feature: FeatureType.Admin, type: FeatureKind.Feature },
activated: true,
},
});
if (count === 0) {
throw new CannotDeleteAllAdminAccount();
}
}
/**
* get user's features, will included inactivated features
* @param userId user id