feat: integrate user usage into apis (#5075)

This commit is contained in:
DarkSky
2023-12-14 09:50:36 +00:00
parent 63de73a815
commit ad23ead5e4
36 changed files with 984 additions and 282 deletions

View File

@@ -1,42 +0,0 @@
type FeatureEarlyAccessPreview = {
whitelist: RegExp[];
};
type FeatureStorageLimit = {
storageQuota: number;
};
type UserFeatureGate = {
earlyAccessPreview: FeatureEarlyAccessPreview;
freeUser: FeatureStorageLimit;
proUser: FeatureStorageLimit;
};
const UserLevel = {
freeUser: {
storageQuota: 10 * 1024 * 1024 * 1024,
},
proUser: {
storageQuota: 100 * 1024 * 1024 * 1024,
},
} satisfies Pick<UserFeatureGate, 'freeUser' | 'proUser'>;
export function getStorageQuota(features: string[]) {
for (const feature of features) {
if (feature in UserLevel) {
return UserLevel[feature as keyof typeof UserLevel].storageQuota;
}
}
return null;
}
const UserType = {
earlyAccessPreview: {
whitelist: [/@toeverything\.info$/],
},
} satisfies Pick<UserFeatureGate, 'earlyAccessPreview'>;
export const FeatureGates = {
...UserType,
...UserLevel,
} satisfies UserFeatureGate;

View File

@@ -1,11 +1,12 @@
import { Module } from '@nestjs/common';
import { FeatureModule } from '../features';
import { StorageModule } from '../storage';
import { UserResolver } from './resolver';
import { UsersService } from './users';
@Module({
imports: [StorageModule],
imports: [StorageModule, FeatureModule],
providers: [UserResolver, UsersService],
exports: [UsersService],
})

View File

@@ -12,7 +12,6 @@ import {
Mutation,
ObjectType,
Query,
registerEnumType,
ResolveField,
Resolver,
} from '@nestjs/graphql';
@@ -24,14 +23,10 @@ import { PrismaService } from '../../prisma/service';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import type { FileUpload } from '../../types';
import { Auth, CurrentUser, Public, Publicable } from '../auth/guard';
import { AuthService } from '../auth/service';
import { FeatureManagementService } from '../features';
import { StorageService } from '../storage/storage.service';
import { NewFeaturesKind } from './types';
import { UsersService } from './users';
import { isStaff } from './utils';
registerEnumType(NewFeaturesKind, {
name: 'NewFeaturesKind',
});
@ObjectType()
export class UserType implements Partial<User> {
@@ -71,14 +66,6 @@ export class RemoveAvatar {
success!: boolean;
}
@ObjectType()
export class AddToNewFeaturesWaitingList {
@Field()
email!: string;
@Field(() => NewFeaturesKind, { description: 'New features kind' })
type!: NewFeaturesKind;
}
/**
* User resolver
* All op rate limit: 10 req/m
@@ -88,9 +75,11 @@ export class AddToNewFeaturesWaitingList {
@Resolver(() => UserType)
export class UserResolver {
constructor(
private readonly auth: AuthService,
private readonly prisma: PrismaService,
private readonly storage: StorageService,
private readonly users: UsersService
private readonly users: UsersService,
private readonly feature: FeatureManagementService
) {}
@Throttle({
@@ -138,7 +127,7 @@ export class UserResolver {
})
@Public()
async user(@Args('email') email: string) {
if (!(await this.users.canEarlyAccess(email))) {
if (!(await this.feature.canEarlyAccess(email))) {
return new GraphQLError(
`You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`,
{
@@ -233,27 +222,55 @@ export class UserResolver {
ttl: 60,
},
})
@Mutation(() => AddToNewFeaturesWaitingList)
async addToNewFeaturesWaitingList(
@CurrentUser() user: UserType,
@Args('type', {
type: () => NewFeaturesKind,
})
type: NewFeaturesKind,
@Mutation(() => Int)
async addToEarlyAccess(
@CurrentUser() currentUser: UserType,
@Args('email') email: string
): Promise<AddToNewFeaturesWaitingList> {
if (!isStaff(user.email)) {
): Promise<number> {
if (!this.feature.isStaff(currentUser.email)) {
throw new ForbiddenException('You are not allowed to do this');
}
await this.prisma.newFeaturesWaitingList.create({
data: {
email,
type,
},
});
return {
email,
type,
};
const user = await this.users.findUserByEmail(email);
if (user) {
return this.feature.addEarlyAccess(user.id);
} else {
const user = await this.auth.createAnonymousUser(email);
return this.feature.addEarlyAccess(user.id);
}
}
@Throttle({
default: {
limit: 10,
ttl: 60,
},
})
@Mutation(() => Int)
async removeEarlyAccess(
@CurrentUser() currentUser: UserType,
@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);
}
@Throttle({
default: {
limit: 10,
ttl: 60,
},
})
@Query(() => [UserType])
async listEarlyAccess(@CurrentUser() user: UserType): Promise<UserType[]> {
if (!this.feature.isStaff(user.email)) {
throw new ForbiddenException('You are not allowed to do this');
}
return this.feature.listEarlyAccess();
}
}

View File

@@ -1,3 +0,0 @@
export enum NewFeaturesKind {
EarlyAccess,
}

View File

@@ -1,54 +1,10 @@
import { Injectable } from '@nestjs/common';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { getStorageQuota } from './gates';
import { NewFeaturesKind } from './types';
import { isStaff } from './utils';
@Injectable()
export class UsersService {
constructor(
private readonly prisma: PrismaService,
private readonly config: Config
) {}
async canEarlyAccess(email: string) {
if (this.config.featureFlags.earlyAccessPreview && !isStaff(email)) {
return this.isEarlyAccessUser(email);
} else {
return true;
}
}
async isEarlyAccessUser(email: string) {
return this.prisma.newFeaturesWaitingList
.count({
where: { email, type: NewFeaturesKind.EarlyAccess },
})
.then(count => count > 0)
.catch(() => false);
}
async getStorageQuotaById(id: string) {
const features = await this.prisma.user
.findUnique({
where: { id },
select: {
features: {
select: {
feature: true,
},
},
},
})
.then(user => user?.features.map(f => f.feature) ?? []);
return (
getStorageQuota(features.map(f => f.feature)) ||
this.config.objectStorage.quota
);
}
constructor(private readonly prisma: PrismaService) {}
async findUserByEmail(email: string) {
return this.prisma.user

View File

@@ -1,3 +0,0 @@
export function isStaff(email: string) {
return email.endsWith('@toeverything.info');
}