feat(server): add administrator feature (#6995)

This commit is contained in:
forehalo
2024-05-27 11:17:21 +00:00
parent 5ba9e2e9b1
commit aff166a0ef
23 changed files with 305 additions and 293 deletions

View File

@@ -1,16 +1,13 @@
import { Module } from '@nestjs/common';
import { FeatureModule } from '../features';
import { QuotaModule } from '../quota';
import { StorageModule } from '../storage';
import { UserAvatarController } from './controller';
import { UserManagementResolver } from './management';
import { UserResolver } from './resolver';
import { UserService } from './service';
@Module({
imports: [StorageModule, FeatureModule, QuotaModule],
providers: [UserResolver, UserManagementResolver, UserService],
imports: [StorageModule],
providers: [UserResolver, UserService],
controllers: [UserAvatarController],
exports: [UserService],
})

View File

@@ -1,78 +0,0 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import {
Args,
Context,
Int,
Mutation,
Query,
registerEnumType,
Resolver,
} from '@nestjs/graphql';
import { CurrentUser } from '../auth/current-user';
import { sessionUser } from '../auth/service';
import { EarlyAccessType, FeatureManagementService } from '../features';
import { UserService } from './service';
import { UserType } from './types';
registerEnumType(EarlyAccessType, {
name: 'EarlyAccessType',
});
@Resolver(() => UserType)
export class UserManagementResolver {
constructor(
private readonly users: UserService,
private readonly feature: FeatureManagementService
) {}
@Mutation(() => Int)
async addToEarlyAccess(
@CurrentUser() currentUser: CurrentUser,
@Args('email') email: string,
@Args({ name: 'type', type: () => EarlyAccessType }) type: EarlyAccessType
): Promise<number> {
if (!this.feature.isStaff(currentUser.email)) {
throw new ForbiddenException('You are not allowed to do this');
}
const user = await this.users.findUserByEmail(email);
if (user) {
return this.feature.addEarlyAccess(user.id, type);
} else {
const user = await this.users.createAnonymousUser(email, {
registered: false,
});
return this.feature.addEarlyAccess(user.id, type);
}
}
@Mutation(() => Int)
async removeEarlyAccess(
@CurrentUser() currentUser: CurrentUser,
@Args('email') email: string
): Promise<number> {
if (!this.feature.isStaff(currentUser.email)) {
throw new ForbiddenException('You are not allowed to do this');
}
const user = await this.users.findUserByEmail(email);
if (!user) {
throw new BadRequestException(`User ${email} not found`);
}
return this.feature.removeEarlyAccess(user.id);
}
@Query(() => [UserType])
async earlyAccessUsers(
@Context() ctx: { isAdminQuery: boolean },
@CurrentUser() user: CurrentUser
): Promise<UserType[]> {
if (!this.feature.isStaff(user.email)) {
throw new ForbiddenException('You are not allowed to do this');
}
// allow query other user's subscription
ctx.isAdminQuery = true;
return this.feature.listEarlyAccess().then(users => {
return users.map(sessionUser);
});
}
}

View File

@@ -7,30 +7,23 @@ import {
ResolveField,
Resolver,
} from '@nestjs/graphql';
import type { User } from '@prisma/client';
import { PrismaClient } from '@prisma/client';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { isNil, omitBy } from 'lodash-es';
import type { FileUpload } from '../../fundamentals';
import {
EventEmitter,
PaymentRequiredException,
Throttle,
} from '../../fundamentals';
import { EventEmitter, Throttle } from '../../fundamentals';
import { CurrentUser } from '../auth/current-user';
import { Public } from '../auth/guard';
import { sessionUser } from '../auth/service';
import { FeatureManagementService, FeatureType } from '../features';
import { QuotaService } from '../quota';
import { AvatarStorage } from '../storage';
import { validators } from '../utils/validators';
import { UserService } from './service';
import {
DeleteAccount,
RemoveAvatar,
UpdateUserInput,
UserOrLimitedUser,
UserQuotaType,
UserType,
} from './types';
@@ -40,8 +33,6 @@ export class UserResolver {
private readonly prisma: PrismaClient,
private readonly storage: AvatarStorage,
private readonly users: UserService,
private readonly feature: FeatureManagementService,
private readonly quota: QuotaService,
private readonly event: EventEmitter
) {}
@@ -53,14 +44,10 @@ export class UserResolver {
})
@Public()
async user(
@CurrentUser() currentUser?: CurrentUser,
@Args('email') email?: string
@Args('email') email: string,
@CurrentUser() currentUser?: CurrentUser
): Promise<typeof UserOrLimitedUser | null> {
if (!email || !(await this.feature.canEarlyAccess(email))) {
throw new PaymentRequiredException(
`You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`
);
}
validators.assertValidEmail(email);
// TODO: need to limit a user can only get another user witch is in the same workspace
const user = await this.users.findUserWithHashedPasswordByEmail(email);
@@ -79,13 +66,6 @@ export class UserResolver {
};
}
@ResolveField(() => UserQuotaType, { name: 'quota', nullable: true })
async getQuota(@CurrentUser() me: User) {
const quota = await this.quota.getUserQuota(me.id);
return quota.feature;
}
@ResolveField(() => Int, {
name: 'invoiceCount',
description: 'Get user invoice count',
@@ -96,14 +76,6 @@ export class UserResolver {
});
}
@ResolveField(() => [FeatureType], {
name: 'features',
description: 'Enabled features of a user',
})
async userFeatures(@CurrentUser() user: CurrentUser) {
return this.feature.getActivatedUserFeatures(user.id);
}
@Mutation(() => UserType, {
name: 'uploadAvatar',
description: 'Upload user avatar',

View File

@@ -6,49 +6,9 @@ import {
ObjectType,
} from '@nestjs/graphql';
import type { User } from '@prisma/client';
import { SafeIntResolver } from 'graphql-scalars';
import { CurrentUser } from '../auth/current-user';
@ObjectType('UserQuotaHumanReadable')
export class UserQuotaHumanReadableType {
@Field({ name: 'name' })
name!: string;
@Field({ name: 'blobLimit' })
blobLimit!: string;
@Field({ name: 'storageQuota' })
storageQuota!: string;
@Field({ name: 'historyPeriod' })
historyPeriod!: string;
@Field({ name: 'memberLimit' })
memberLimit!: string;
}
@ObjectType('UserQuota')
export class UserQuotaType {
@Field({ name: 'name' })
name!: string;
@Field(() => SafeIntResolver, { name: 'blobLimit' })
blobLimit!: number;
@Field(() => SafeIntResolver, { name: 'storageQuota' })
storageQuota!: number;
@Field(() => SafeIntResolver, { name: 'historyPeriod' })
historyPeriod!: number;
@Field({ name: 'memberLimit' })
memberLimit!: number;
@Field({ name: 'humanReadable' })
humanReadable!: UserQuotaHumanReadableType;
}
@ObjectType()
export class UserType implements CurrentUser {
@Field(() => ID)