fix(admin): organize admin panel (#7840)
This commit is contained in:
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user