Skip to content

Commit

Permalink
fix(admin): organize admin panel (#7840)
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo committed Aug 13, 2024
1 parent 6dea831 commit 0ec1995
Show file tree
Hide file tree
Showing 45 changed files with 740 additions and 949 deletions.
3 changes: 1 addition & 2 deletions packages/backend/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ function buildAppModule() {
.useIf(config => config.flavor.sync, WebSocketModule)

// auth
.use(AuthModule)
.use(UserModule, AuthModule)

// business modules
.use(DocModule)
Expand All @@ -169,7 +169,6 @@ function buildAppModule() {
ServerConfigModule,
GqlModule,
StorageModule,
UserModule,
WorkspaceModule,
FeatureModule,
QuotaModule
Expand Down
14 changes: 6 additions & 8 deletions packages/backend/server/src/core/auth/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Cron, CronExpression } from '@nestjs/schedule';
import type { User, UserSession } from '@prisma/client';
import { PrismaClient } from '@prisma/client';
import type { CookieOptions, Request, Response } from 'express';
import { assign, omit } from 'lodash-es';
import { assign, pick } from 'lodash-es';

import { Config, EmailAlreadyUsed, MailService } from '../../fundamentals';
import { FeatureManagementService } from '../features/management';
Expand Down Expand Up @@ -41,13 +41,11 @@ export function sessionUser(
'id' | 'email' | 'avatarUrl' | 'name' | 'emailVerifiedAt'
> & { password?: string | null }
): CurrentUser {
return assign(
omit(user, 'password', 'registered', 'emailVerifiedAt', 'createdAt'),
{
hasPassword: user.password !== null,
emailVerified: user.emailVerifiedAt !== null,
}
);
// use pick to avoid unexpected fields
return assign(pick(user, 'id', 'email', 'avatarUrl', 'name'), {
hasPassword: user.password !== null,
emailVerified: user.emailVerifiedAt !== null,
});
}

@Injectable()
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/server/src/core/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ import {
],
})
export class ServerConfigModule {}
export { ADD_ENABLED_FEATURES, ServerConfigType } from './resolver';
export { ADD_ENABLED_FEATURES } from './server-feature';
export { ServerFeature } from './types';
64 changes: 19 additions & 45 deletions packages/backend/server/src/core/config/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,14 @@ import {
import { PrismaClient, RuntimeConfig, RuntimeConfigType } from '@prisma/client';
import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';

import { Config, DeploymentType, URLHelper } from '../../fundamentals';
import { Config, URLHelper } from '../../fundamentals';
import { Public } from '../auth';
import { Admin } from '../common';
import { FeatureType } from '../features';
import { AvailableUserFeatureConfig } from '../features/resolver';
import { ServerFlags } from './config';
import { ServerFeature } from './types';

const ENABLED_FEATURES: Set<ServerFeature> = new Set();
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
ENABLED_FEATURES.add(feature);
}

registerEnumType(ServerFeature, {
name: 'ServerFeature',
});

registerEnumType(DeploymentType, {
name: 'ServerDeploymentType',
});
import { ENABLED_FEATURES } from './server-feature';
import { ServerConfigType } from './types';

@ObjectType()
export class PasswordLimitsType {
Expand All @@ -45,36 +35,6 @@ export class CredentialsRequirementType {
password!: PasswordLimitsType;
}

@ObjectType()
export class ServerConfigType {
@Field({
description:
'server identical name could be shown as badge on user interface',
})
name!: string;

@Field({ description: 'server version' })
version!: string;

@Field({ description: 'server base url' })
baseUrl!: string;

@Field(() => DeploymentType, { description: 'server type' })
type!: DeploymentType;

/**
* @deprecated
*/
@Field({ description: 'server flavor', deprecationReason: 'use `features`' })
flavor!: string;

@Field(() => [ServerFeature], { description: 'enabled server features' })
features!: ServerFeature[];

@Field({ description: 'enable telemetry' })
enableTelemetry!: boolean;
}

registerEnumType(RuntimeConfigType, {
name: 'RuntimeConfigType',
});
Expand Down Expand Up @@ -175,6 +135,20 @@ export class ServerConfigResolver {
}
}

@Resolver(() => ServerConfigType)
export class ServerFeatureConfigResolver extends AvailableUserFeatureConfig {
constructor(config: Config) {
super(config);
}

@ResolveField(() => [FeatureType], {
description: 'Features for user that can be configured',
})
override availableUserFeatures() {
return super.availableUserFeatures();
}
}

@ObjectType()
class ServerServiceConfig {
@Field()
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/server/src/core/config/server-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ServerFeature } from './types';

export const ENABLED_FEATURES: Set<ServerFeature> = new Set();
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
ENABLED_FEATURES.add(feature);
}
export { ServerFeature };
42 changes: 42 additions & 0 deletions packages/backend/server/src/core/config/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';

import { DeploymentType } from '../../fundamentals';

export enum ServerFeature {
Copilot = 'copilot',
Payment = 'payment',
OAuth = 'oauth',
}

registerEnumType(ServerFeature, {
name: 'ServerFeature',
});

registerEnumType(DeploymentType, {
name: 'ServerDeploymentType',
});

@ObjectType()
export class ServerConfigType {
@Field({
description:
'server identical name could be shown as badge on user interface',
})
name!: string;

@Field({ description: 'server version' })
version!: string;

@Field({ description: 'server base url' })
baseUrl!: string;

@Field(() => DeploymentType, { description: 'server type' })
type!: DeploymentType;

/**
* @deprecated
*/
@Field({ description: 'server flavor', deprecationReason: 'use `features`' })
flavor!: string;

@Field(() => [ServerFeature], { description: 'enabled server features' })
features!: ServerFeature[];

@Field({ description: 'enable telemetry' })
enableTelemetry!: boolean;
}
6 changes: 5 additions & 1 deletion packages/backend/server/src/core/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -17,6 +20,7 @@ import { FeatureService } from './service';
FeatureService,
FeatureManagementService,
FeatureManagementResolver,
AdminFeatureManagementResolver,
],
exports: [FeatureService, FeatureManagementService],
})
Expand Down
4 changes: 0 additions & 4 deletions packages/backend/server/src/core/features/management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
111 changes: 39 additions & 72 deletions packages/backend/server/src/core/features/resolver.ts
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -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',
Expand All @@ -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);
}
}

@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);
}
export class AvailableUserFeatureConfig {
constructor(private readonly config: Config) {}

@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);
});
async availableUserFeatures() {
return this.config.isSelfhosted
? [FeatureType.Admin]
: [FeatureType.EarlyAccess, FeatureType.AIEarlyAccess, FeatureType.Admin];
}
}

@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()
@Resolver(() => Boolean)
export class AdminFeatureManagementResolver extends AvailableUserFeatureConfig {
constructor(
config: Config,
private readonly feature: FeatureService
) {
super(config);
}

@Admin()
@Mutation(() => Boolean)
async removeAdminister(@Args('email') email: string): Promise<boolean> {
const user = await this.users.findUserByEmail(email);

if (!user) {
throw new UserNotFound();
}
@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();

await this.feature.removeAdmin(user.id);
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 true;
return features;
}
}
Loading

0 comments on commit 0ec1995

Please sign in to comment.