feat: add workspace experimental features api (#5525)

This commit is contained in:
DarkSky
2024-01-06 11:04:49 +00:00
parent 9650a5a6a1
commit 443908da22
10 changed files with 259 additions and 59 deletions

View File

@@ -19,7 +19,13 @@ import { Auth, CurrentUser, Public, Publicable } from '../auth/guard';
import { FeatureManagementService } from '../features';
import { QuotaService } from '../quota';
import { AvatarStorage } from '../storage';
import { DeleteAccount, RemoveAvatar, UserQuotaType, UserType } from './types';
import {
DeleteAccount,
RemoveAvatar,
UserOrLimitedUser,
UserQuotaType,
UserType,
} from './types';
import { UsersService } from './users';
/**
@@ -77,14 +83,17 @@ export class UserResolver {
ttl: 60,
},
})
@Query(() => UserType, {
@Query(() => UserOrLimitedUser, {
name: 'user',
description: 'Get user by email',
nullable: true,
})
@Public()
async user(@Args('email') email: string) {
if (!(await this.feature.canEarlyAccess(email))) {
async user(
@CurrentUser() currentUser?: UserType,
@Args('email') email?: string
) {
if (!email || !(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`,
{
@@ -95,13 +104,16 @@ export class UserResolver {
}
);
}
// TODO: need to limit a user can only get another user witch is in the same workspace
const user = await this.users.findUserByEmail(email);
if (user?.password) {
const userResponse: UserType = user;
userResponse.hasPassword = true;
}
return user;
if (currentUser) return user;
// only return limited info when not logged in
return {
email: user?.email,
hasPassword: !!user?.password,
};
}
@Throttle({ default: { limit: 10, ttl: 60 } })

View File

@@ -1,4 +1,4 @@
import { Field, Float, ID, ObjectType } from '@nestjs/graphql';
import { createUnionType, Field, Float, ID, ObjectType } from '@nestjs/graphql';
import type { User } from '@prisma/client';
@ObjectType('UserQuotaHumanReadable')
@@ -67,6 +67,29 @@ export class UserType implements Partial<User> {
hasPassword?: boolean;
}
@ObjectType()
export class LimitedUserType implements Partial<User> {
@Field({ description: 'User email' })
email!: string;
@Field(() => Boolean, {
description: 'User password has been set',
nullable: true,
})
hasPassword?: boolean;
}
export const UserOrLimitedUser = createUnionType({
name: 'UserOrLimitedUser',
types: () => [UserType, LimitedUserType] as const,
resolveType(value) {
if (value.id) {
return UserType;
}
return LimitedUserType;
},
});
@ObjectType()
export class DeleteAccount {
@Field()

View File

@@ -13,13 +13,17 @@ import { CloudThrottlerGuard, Throttle } from '../../throttler';
import { Auth, CurrentUser } from '../auth';
import { FeatureManagementService, FeatureType } from '../features';
import { UserType } from '../users';
import { PermissionService } from './permission';
import { WorkspaceType } from './types';
@UseGuards(CloudThrottlerGuard)
@Auth()
@Resolver(() => WorkspaceType)
export class WorkspaceManagementResolver {
constructor(private readonly feature: FeatureManagementService) {}
constructor(
private readonly feature: FeatureManagementService,
private readonly permission: PermissionService
) {}
@Throttle({
default: {
@@ -77,6 +81,51 @@ export class WorkspaceManagementResolver {
return this.feature.listFeatureWorkspaces(feature);
}
@Mutation(() => Boolean)
async setWorkspaceExperimentalFeature(
@CurrentUser() user: UserType,
@Args('workspaceId') workspaceId: string,
@Args('feature', { type: () => FeatureType }) feature: FeatureType,
@Args('enable') enable: boolean
): Promise<boolean> {
if (!(await this.feature.canEarlyAccess(user.email))) {
throw new ForbiddenException('You are not allowed to do this');
}
const owner = await this.permission.getWorkspaceOwner(workspaceId);
const availableFeatures = await this.availableFeatures(user);
if (owner.user.id !== user.id || !availableFeatures.includes(feature)) {
throw new ForbiddenException('You are not allowed to do this');
}
if (enable) {
return await this.feature
.addWorkspaceFeatures(
workspaceId,
feature,
undefined,
'add by experimental feature api'
)
.then(id => id > 0);
} else {
return await this.feature.removeWorkspaceFeature(workspaceId, feature);
}
}
@ResolveField(() => [FeatureType], {
description: 'Available features of workspace',
complexity: 2,
})
async availableFeatures(
@CurrentUser() user: UserType
): Promise<FeatureType[]> {
if (await this.feature.canEarlyAccess(user.email)) {
return [FeatureType.Copilot];
} else {
return [];
}
}
@ResolveField(() => [FeatureType], {
description: 'Enabled features of workspace',
complexity: 2,

View File

@@ -131,6 +131,9 @@ type WorkspaceType {
"""Owner of workspace"""
owner: UserType!
"""Available features of workspace"""
availableFeatures: [FeatureType!]!
"""Enabled features of workspace"""
features: [FeatureType!]!
@@ -299,11 +302,21 @@ type Query {
currentUser: UserType
"""Get user by email"""
user(email: String!): UserType
user(email: String!): UserOrLimitedUser
earlyAccessUsers: [UserType!]!
prices: [SubscriptionPrice!]!
}
union UserOrLimitedUser = UserType | LimitedUserType
type LimitedUserType {
"""User email"""
email: String!
"""User password has been set"""
hasPassword: Boolean
}
type Mutation {
signUp(name: String!, email: String!, password: String!): UserType!
signIn(email: String!, password: String!): UserType!
@@ -326,6 +339,7 @@ type Mutation {
leaveWorkspace(workspaceId: String!, workspaceName: String!, sendLeaveMail: Boolean): Boolean!
addWorkspaceFeature(workspaceId: String!, feature: FeatureType!): Int!
removeWorkspaceFeature(workspaceId: String!, feature: FeatureType!): Int!
setWorkspaceExperimentalFeature(workspaceId: String!, feature: FeatureType!, enable: Boolean!): Boolean!
sharePage(workspaceId: String!, pageId: String!): Boolean! @deprecated(reason: "renamed to publicPage")
publishPage(workspaceId: String!, pageId: String!, mode: PublicPageMode = Page): WorkspacePage!
revokePage(workspaceId: String!, pageId: String!): Boolean! @deprecated(reason: "use revokePublicPage")

View File

@@ -114,8 +114,12 @@ test('should find default user', async t => {
query: `
query {
user(email: "alex.yang@example.org") {
email
avatarUrl
... on UserType {
email
}
... on LimitedUserType {
email
}
}
}
`,