From 0ec1995addbb09fb5d4af765d84cc914b2905150 Mon Sep 17 00:00:00 2001 From: forehalo Date: Tue, 13 Aug 2024 05:45:02 +0000 Subject: [PATCH] fix(admin): organize admin panel (#7840) --- packages/backend/server/src/app.module.ts | 3 +- .../backend/server/src/core/auth/service.ts | 14 +- .../backend/server/src/core/config/index.ts | 2 +- .../server/src/core/config/resolver.ts | 64 ++-- .../server/src/core/config/server-feature.ts | 7 + .../backend/server/src/core/config/types.ts | 42 +++ .../backend/server/src/core/features/index.ts | 6 +- .../server/src/core/features/management.ts | 4 - .../server/src/core/features/resolver.ts | 123 +++----- .../server/src/core/features/service.ts | 18 ++ .../backend/server/src/core/user/resolver.ts | 35 ++- .../backend/server/src/core/user/service.ts | 21 +- .../backend/server/src/core/user/types.ts | 6 +- .../server/src/fundamentals/error/def.ts | 8 + .../src/fundamentals/error/errors.gen.ts | 16 +- .../server/src/plugins/oauth/resolver.ts | 2 +- packages/backend/server/src/schema.gql | 18 +- .../accounts/components/ceate-user-panel.tsx | 134 -------- .../modules/accounts/components/columns.tsx | 14 +- .../components/data-table-row-actions.tsx | 24 +- .../components/data-table-toolbar.tsx | 9 +- .../accounts/components/edit-panel.tsx | 155 ---------- .../components/use-user-management.ts | 195 ++++-------- .../modules/accounts/components/user-form.tsx | 288 ++++++++++++++++++ .../admin/src/modules/accounts/index.tsx | 1 + .../admin/src/modules/accounts/schema.ts | 40 +-- .../frontend/admin/src/modules/auth/index.tsx | 16 +- packages/frontend/admin/src/modules/common.ts | 21 ++ .../admin/src/modules/config/index.tsx | 4 +- .../frontend/admin/src/modules/layout.tsx | 24 +- .../admin/src/modules/nav/user-dropdown.tsx | 22 +- .../frontend/admin/src/modules/setup/form.tsx | 14 +- .../frontend/core/src/hooks/use-mutation.ts | 4 +- packages/frontend/core/src/hooks/use-query.ts | 8 +- .../graphql/src/graphql/add-admin.gql | 3 - .../src/graphql/admin-server-config.gql | 17 ++ .../graphql/src/graphql/early-access-add.gql | 3 - .../graphql/src/graphql/early-access-list.gql | 16 - .../src/graphql/early-access-remove.gql | 3 - .../frontend/graphql/src/graphql/index.ts | 104 ++----- .../graphql/src/graphql/list-users.gql | 9 - .../graphql/src/graphql/remove-admin.gql | 3 - .../graphql/src/graphql/server-config.gql | 1 - .../src/graphql/update-account-features.gql | 3 + packages/frontend/graphql/src/schema.ts | 177 ++++------- 45 files changed, 746 insertions(+), 955 deletions(-) create mode 100644 packages/backend/server/src/core/config/server-feature.ts delete mode 100644 packages/frontend/admin/src/modules/accounts/components/ceate-user-panel.tsx delete mode 100644 packages/frontend/admin/src/modules/accounts/components/edit-panel.tsx create mode 100644 packages/frontend/admin/src/modules/accounts/components/user-form.tsx create mode 100644 packages/frontend/admin/src/modules/common.ts delete mode 100644 packages/frontend/graphql/src/graphql/add-admin.gql create mode 100644 packages/frontend/graphql/src/graphql/admin-server-config.gql delete mode 100644 packages/frontend/graphql/src/graphql/early-access-add.gql delete mode 100644 packages/frontend/graphql/src/graphql/early-access-list.gql delete mode 100644 packages/frontend/graphql/src/graphql/early-access-remove.gql delete mode 100644 packages/frontend/graphql/src/graphql/remove-admin.gql create mode 100644 packages/frontend/graphql/src/graphql/update-account-features.gql diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts index 45f86a2d54..ac80a57801 100644 --- a/packages/backend/server/src/app.module.ts +++ b/packages/backend/server/src/app.module.ts @@ -155,7 +155,7 @@ function buildAppModule() { .useIf(config => config.flavor.sync, WebSocketModule) // auth - .use(AuthModule) + .use(UserModule, AuthModule) // business modules .use(DocModule) @@ -169,7 +169,6 @@ function buildAppModule() { ServerConfigModule, GqlModule, StorageModule, - UserModule, WorkspaceModule, FeatureModule, QuotaModule diff --git a/packages/backend/server/src/core/auth/service.ts b/packages/backend/server/src/core/auth/service.ts index c372bb80c1..81191d2b4b 100644 --- a/packages/backend/server/src/core/auth/service.ts +++ b/packages/backend/server/src/core/auth/service.ts @@ -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'; @@ -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() diff --git a/packages/backend/server/src/core/config/index.ts b/packages/backend/server/src/core/config/index.ts index 85df6d38a8..95201529ab 100644 --- a/packages/backend/server/src/core/config/index.ts +++ b/packages/backend/server/src/core/config/index.ts @@ -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'; diff --git a/packages/backend/server/src/core/config/resolver.ts b/packages/backend/server/src/core/config/resolver.ts index 7a1a64c217..cd78c10d47 100644 --- a/packages/backend/server/src/core/config/resolver.ts +++ b/packages/backend/server/src/core/config/resolver.ts @@ -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 = 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 { @@ -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', }); @@ -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() diff --git a/packages/backend/server/src/core/config/server-feature.ts b/packages/backend/server/src/core/config/server-feature.ts new file mode 100644 index 0000000000..44375bb914 --- /dev/null +++ b/packages/backend/server/src/core/config/server-feature.ts @@ -0,0 +1,7 @@ +import { ServerFeature } from './types'; + +export const ENABLED_FEATURES: Set = new Set(); +export function ADD_ENABLED_FEATURES(feature: ServerFeature) { + ENABLED_FEATURES.add(feature); +} +export { ServerFeature }; diff --git a/packages/backend/server/src/core/config/types.ts b/packages/backend/server/src/core/config/types.ts index c8745b7f8a..169239ce16 100644 --- a/packages/backend/server/src/core/config/types.ts +++ b/packages/backend/server/src/core/config/types.ts @@ -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; +} diff --git a/packages/backend/server/src/core/features/index.ts b/packages/backend/server/src/core/features/index.ts index 3aaac50da7..054cbbf100 100644 --- a/packages/backend/server/src/core/features/index.ts +++ b/packages/backend/server/src/core/features/index.ts @@ -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], }) diff --git a/packages/backend/server/src/core/features/management.ts b/packages/backend/server/src/core/features/management.ts index aa838f69d1..9ff93bb5b7 100644 --- a/packages/backend/server/src/core/features/management.ts +++ b/packages/backend/server/src/core/features/management.ts @@ -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, diff --git a/packages/backend/server/src/core/features/resolver.ts b/packages/backend/server/src/core/features/resolver.ts index 835301588d..34096f3dce 100644 --- a/packages/backend/server/src/core/features/resolver.ts +++ b/packages/backend/server/src/core/features/resolver.ts @@ -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 { - 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 { - 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 { - // 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 { - 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 { - 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; } } diff --git a/packages/backend/server/src/core/features/service.ts b/packages/backend/server/src/core/features/service.ts index 3dd60a6cce..c885e06074 100644 --- a/packages/backend/server/src/core/features/service.ts +++ b/packages/backend/server/src/core/features/service.ts @@ -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 diff --git a/packages/backend/server/src/core/user/resolver.ts b/packages/backend/server/src/core/user/resolver.ts index 90fba524e3..348b3b52ca 100644 --- a/packages/backend/server/src/core/user/resolver.ts +++ b/packages/backend/server/src/core/user/resolver.ts @@ -12,7 +12,12 @@ import { PrismaClient } from '@prisma/client'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { isNil, omitBy } from 'lodash-es'; -import { type FileUpload, Throttle, UserNotFound } from '../../fundamentals'; +import { + CannotDeleteOwnAccount, + type FileUpload, + Throttle, + UserNotFound, +} from '../../fundamentals'; import { CurrentUser } from '../auth/current-user'; import { Public } from '../auth/guard'; import { sessionUser } from '../auth/service'; @@ -162,9 +167,6 @@ class CreateUserInput { @Field(() => String, { nullable: true }) name!: string | null; - - @Field(() => String, { nullable: true }) - password!: string | null; } @Admin() @@ -244,7 +246,6 @@ export class UserManagementResolver { ) { const { id } = await this.user.createUser({ email: input.email, - password: input.password, registered: true, }); @@ -255,7 +256,13 @@ export class UserManagementResolver { @Mutation(() => DeleteAccount, { description: 'Delete a user account', }) - async deleteUser(@Args('id') id: string): Promise { + async deleteUser( + @CurrentUser() user: CurrentUser, + @Args('id') id: string + ): Promise { + if (user.id === id) { + throw new CannotDeleteOwnAccount(); + } await this.user.deleteUser(id); return { success: true }; } @@ -268,26 +275,22 @@ export class UserManagementResolver { @Args('input') input: ManageUserInput ): Promise { const user = await this.db.user.findUnique({ - select: { ...this.user.defaultUserSelect, password: true }, where: { id }, }); if (!user) { throw new UserNotFound(); } - validators.assertValidEmail(input.email); - if (input.email !== user.email) { - const exists = await this.db.user.findFirst({ - where: { email: input.email }, - }); - if (exists) { - throw new Error('Email already exists'); - } + + input = omitBy(input, isNil); + if (Object.keys(input).length === 0) { + return sessionUser(user); } + return sessionUser( await this.user.updateUser(user.id, { - name: input.name, email: input.email, + name: input.name, }) ); } diff --git a/packages/backend/server/src/core/user/service.ts b/packages/backend/server/src/core/user/service.ts index be6e824945..42cb20035b 100644 --- a/packages/backend/server/src/core/user/service.ts +++ b/packages/backend/server/src/core/user/service.ts @@ -194,9 +194,7 @@ export class UserService { async updateUser( id: string, - data: Omit & { - password?: string | null; - }, + data: Omit, 'id'>, select: Prisma.UserSelect = this.defaultUserSelect ) { if (data.password) { @@ -211,6 +209,23 @@ export class UserService { data.password = await this.crypto.encryptPassword(data.password); } + + if (data.email) { + validators.assertValidEmail(data.email); + const emailTaken = await this.prisma.user.count({ + where: { + email: data.email, + id: { + not: id, + }, + }, + }); + + if (emailTaken) { + throw new EmailAlreadyUsed(); + } + } + const user = await this.prisma.user.update({ where: { id }, data, select }); this.emitter.emit('user.updated', user); diff --git a/packages/backend/server/src/core/user/types.ts b/packages/backend/server/src/core/user/types.ts index 463715ca67..b0d7192b34 100644 --- a/packages/backend/server/src/core/user/types.ts +++ b/packages/backend/server/src/core/user/types.ts @@ -85,11 +85,11 @@ export class UpdateUserInput implements Partial { @InputType() export class ManageUserInput { + @Field({ description: 'User email', nullable: true }) + email?: string; + @Field({ description: 'User name', nullable: true }) name?: string; - - @Field({ description: 'User email' }) - email!: string; } declare module '../../fundamentals/event/def' { diff --git a/packages/backend/server/src/fundamentals/error/def.ts b/packages/backend/server/src/fundamentals/error/def.ts index facff3cbb8..003ba03c7b 100644 --- a/packages/backend/server/src/fundamentals/error/def.ts +++ b/packages/backend/server/src/fundamentals/error/def.ts @@ -498,4 +498,12 @@ export const USER_FRIENDLY_ERRORS = { type: 'internal_server_error', message: 'Mailer service is not configured.', }, + cannot_delete_all_admin_account: { + type: 'action_forbidden', + message: 'Cannot delete all admin accounts.', + }, + cannot_delete_own_account: { + type: 'action_forbidden', + message: 'Cannot delete own account.', + }, } satisfies Record; diff --git a/packages/backend/server/src/fundamentals/error/errors.gen.ts b/packages/backend/server/src/fundamentals/error/errors.gen.ts index a793399d45..883b9c770a 100644 --- a/packages/backend/server/src/fundamentals/error/errors.gen.ts +++ b/packages/backend/server/src/fundamentals/error/errors.gen.ts @@ -487,6 +487,18 @@ export class MailerServiceIsNotConfigured extends UserFriendlyError { super('internal_server_error', 'mailer_service_is_not_configured', message); } } + +export class CannotDeleteAllAdminAccount extends UserFriendlyError { + constructor(message?: string) { + super('action_forbidden', 'cannot_delete_all_admin_account', message); + } +} + +export class CannotDeleteOwnAccount extends UserFriendlyError { + constructor(message?: string) { + super('action_forbidden', 'cannot_delete_own_account', message); + } +} export enum ErrorNames { INTERNAL_SERVER_ERROR, TOO_MANY_REQUEST, @@ -551,7 +563,9 @@ export enum ErrorNames { COPILOT_QUOTA_EXCEEDED, RUNTIME_CONFIG_NOT_FOUND, INVALID_RUNTIME_CONFIG_TYPE, - MAILER_SERVICE_IS_NOT_CONFIGURED + MAILER_SERVICE_IS_NOT_CONFIGURED, + CANNOT_DELETE_ALL_ADMIN_ACCOUNT, + CANNOT_DELETE_OWN_ACCOUNT } registerEnumType(ErrorNames, { name: 'ErrorNames' diff --git a/packages/backend/server/src/plugins/oauth/resolver.ts b/packages/backend/server/src/plugins/oauth/resolver.ts index 8d0da0c682..02cd5f9e0e 100644 --- a/packages/backend/server/src/plugins/oauth/resolver.ts +++ b/packages/backend/server/src/plugins/oauth/resolver.ts @@ -1,6 +1,6 @@ import { registerEnumType, ResolveField, Resolver } from '@nestjs/graphql'; -import { ServerConfigType } from '../../core/config'; +import { ServerConfigType } from '../../core/config/types'; import { OAuthProviderName } from './config'; import { OAuthProviderFactory } from './register'; diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 58d6a2f01d..0054525416 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -152,7 +152,6 @@ input CreateCopilotPromptInput { input CreateUserInput { email: String! name: String - password: String } type CredentialsRequirementType { @@ -196,11 +195,6 @@ type DocNotFoundDataType { workspaceId: String! } -enum EarlyAccessType { - AI - App -} - union ErrorDataUnion = BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MissingOauthQueryParameterDataType | NotInWorkspaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | VersionRejectedDataType | WorkspaceAccessDeniedDataType | WorkspaceNotFoundDataType | WorkspaceOwnerNotFoundDataType enum ErrorNames { @@ -209,6 +203,8 @@ enum ErrorNames { AUTHENTICATION_REQUIRED BLOB_NOT_FOUND BLOB_QUOTA_EXCEEDED + CANNOT_DELETE_ALL_ADMIN_ACCOUNT + CANNOT_DELETE_OWN_ACCOUNT CANT_CHANGE_WORKSPACE_OWNER CANT_UPDATE_LIFETIME_SUBSCRIPTION COPILOT_ACTION_TAKEN @@ -398,7 +394,7 @@ input ListUserInput { input ManageUserInput { """User email""" - email: String! + email: String """User name""" name: String @@ -410,8 +406,6 @@ type MissingOauthQueryParameterDataType { type Mutation { acceptInviteById(inviteId: String!, sendAcceptMail: Boolean, workspaceId: String!): Boolean! - addAdminister(email: String!): Boolean! - addToEarlyAccess(email: String!, type: EarlyAccessType!): Int! addWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int! cancelSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription! changeEmail(email: String!, token: String!): UserType! @@ -456,11 +450,9 @@ type Mutation { leaveWorkspace(sendLeaveMail: Boolean, workspaceId: String!, workspaceName: String!): Boolean! publishPage(mode: PublicPageMode = Page, pageId: String!, workspaceId: String!): WorkspacePage! recoverDoc(guid: String!, timestamp: DateTime!, workspaceId: String!): DateTime! - removeAdminister(email: String!): Boolean! """Remove user avatar""" removeAvatar: RemoveAvatar! - removeEarlyAccess(email: String!, type: EarlyAccessType!): Int! removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int! resumeSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription! revoke(userId: String!, workspaceId: String!): Boolean! @@ -489,6 +481,9 @@ type Mutation { """Update a user""" updateUser(id: String!, input: ManageUserInput!): UserType! + """update user enabled feature""" + updateUserFeatures(features: [FeatureType!]!, id: String!): [FeatureType!]! + """Update workspace""" updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType! @@ -532,7 +527,6 @@ type Query { """Get current user""" currentUser: UserType - earlyAccessUsers: [UserType!]! error(name: ErrorNames!): ErrorDataUnion! """send workspace invitation""" diff --git a/packages/frontend/admin/src/modules/accounts/components/ceate-user-panel.tsx b/packages/frontend/admin/src/modules/accounts/components/ceate-user-panel.tsx deleted file mode 100644 index fe3d5915e5..0000000000 --- a/packages/frontend/admin/src/modules/accounts/components/ceate-user-panel.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { Button } from '@affine/admin/components/ui/button'; -import { Input } from '@affine/admin/components/ui/input'; -import { Label } from '@affine/admin/components/ui/label'; -import { Separator } from '@affine/admin/components/ui/separator'; -import { Switch } from '@affine/admin/components/ui/switch'; -import { FeatureType } from '@affine/graphql'; -import { CheckIcon, XIcon } from 'lucide-react'; -import { useCallback, useState } from 'react'; - -import { useRightPanel } from '../../layout'; -import { useUserManagement } from './use-user-management'; - -export function CreateUserPanel() { - const { closePanel } = useRightPanel(); - const [name, setName] = useState(''); - const [password, setPassword] = useState(''); - const [email, setEmail] = useState(''); - const [features, setFeatures] = useState([]); - - const disableSave = !name || !email; - - const { createUser } = useUserManagement(); - - const handleConfirm = useCallback(() => { - createUser({ - name, - email, - password, - features, - callback: closePanel, - }); - }, [closePanel, createUser, email, features, name, password]); - - const onEarlyAccessChange = useCallback( - (checked: boolean) => { - setFeatures( - checked - ? [...features, FeatureType.AIEarlyAccess] - : features.filter(f => f !== FeatureType.AIEarlyAccess) - ); - }, - [features] - ); - - const onAdminChange = useCallback( - (checked: boolean) => { - setFeatures( - checked - ? [...features, FeatureType.Admin] - : features.filter(f => f !== FeatureType.Admin) - ); - }, - [features] - ); - - return ( -
-
- - Create Account - -
- -
-
-
- - setName(e.target.value)} - /> -
- -
- - setEmail(e.target.value)} - /> -
{' '} - -
- - setPassword(e.target.value)} - /> -
-
- -
- - - -
-
-
- ); -} diff --git a/packages/frontend/admin/src/modules/accounts/components/columns.tsx b/packages/frontend/admin/src/modules/accounts/components/columns.tsx index a5c8b86322..0f562ddce9 100644 --- a/packages/frontend/admin/src/modules/accounts/components/columns.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/columns.tsx @@ -3,6 +3,7 @@ import { AvatarFallback, AvatarImage, } from '@affine/admin/components/ui/avatar'; +import type { UserType } from '@affine/graphql'; import { FeatureType } from '@affine/graphql'; import type { ColumnDef } from '@tanstack/react-table'; import clsx from 'clsx'; @@ -15,7 +16,6 @@ import { } from 'lucide-react'; import type { ReactNode } from 'react'; -import type { User } from '../schema'; import { DataTableRowActions } from './data-table-row-actions'; const StatusItem = ({ @@ -51,7 +51,7 @@ const StatusItem = ({ ); -export const columns: ColumnDef[] = [ +export const columns: ColumnDef[] = [ { accessorKey: 'info', cell: ({ row }) => ( @@ -88,13 +88,13 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'property', - cell: ({ row }) => ( + cell: ({ row: { original: user } }) => (
-
{row.original.id}
+
{user.id}
} IconFalse={} textTrue="Password Set" @@ -102,7 +102,7 @@ export const columns: ColumnDef[] = [ /> } IconFalse={} textTrue="Email Verified" @@ -110,7 +110,7 @@ export const columns: ColumnDef[] = [ />
- +
), }, diff --git a/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx b/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx index 1c5c99421a..9d604eeccc 100644 --- a/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx @@ -6,7 +6,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@affine/admin/components/ui/dropdown-menu'; -import type { Row } from '@tanstack/react-table'; import { LockIcon, MoreVerticalIcon, @@ -17,29 +16,26 @@ import { useCallback, useState } from 'react'; import { toast } from 'sonner'; import { useRightPanel } from '../../layout'; -import { userSchema } from '../schema'; +import type { UserType } from '../schema'; import { DeleteAccountDialog } from './delete-account'; import { DiscardChanges } from './discard-changes'; -import { EditPanel } from './edit-panel'; import { ResetPasswordDialog } from './reset-password'; -import { useUserManagement } from './use-user-management'; +import { useDeleteUser, useResetUserPassword } from './use-user-management'; +import { UpdateUserForm } from './user-form'; -interface DataTableRowActionsProps { - row: Row; +interface DataTableRowActionsProps { + user: UserType; } -export function DataTableRowActions({ - row, -}: DataTableRowActionsProps) { +export function DataTableRowActions({ user }: DataTableRowActionsProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false); const [discardDialogOpen, setDiscardDialogOpen] = useState(false); - const user = userSchema.parse(row.original); const { setRightPanelContent, openPanel, isOpen, closePanel } = useRightPanel(); - const { deleteUser, resetPasswordLink, onResetPassword } = - useUserManagement(); + const deleteUser = useDeleteUser(); + const { resetPasswordLink, onResetPassword } = useResetUserPassword(); const openResetPasswordDialog = useCallback(() => { onResetPassword(user.id, () => setResetPasswordDialogOpen(true)); @@ -82,8 +78,9 @@ export function DataTableRowActions({ const handleConfirm = useCallback(() => { setRightPanelContent( - @@ -96,6 +93,7 @@ export function DataTableRowActions({ openPanel(); } }, [ + closePanel, discardDialogOpen, handleDiscardChangesCancel, isOpen, diff --git a/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx b/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx index 47929cbb28..dba7c34803 100644 --- a/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx @@ -7,8 +7,8 @@ import type { SetStateAction } from 'react'; import { startTransition, useCallback, useEffect, useState } from 'react'; import { useRightPanel } from '../../layout'; -import { CreateUserPanel } from './ceate-user-panel'; import { DiscardChanges } from './discard-changes'; +import { CreateUserForm } from './user-form'; interface DataTableToolbarProps { data: TData[]; @@ -38,17 +38,18 @@ export function DataTableToolbar({ const [value, setValue] = useState(''); const [dialogOpen, setDialogOpen] = useState(false); const debouncedValue = useDebouncedValue(value, 500); - const { setRightPanelContent, openPanel, isOpen } = useRightPanel(); + const { setRightPanelContent, openPanel, closePanel, isOpen } = + useRightPanel(); const handleConfirm = useCallback(() => { - setRightPanelContent(); + setRightPanelContent(); if (dialogOpen) { setDialogOpen(false); } if (!isOpen) { openPanel(); } - }, [setRightPanelContent, dialogOpen, isOpen, openPanel]); + }, [setRightPanelContent, closePanel, dialogOpen, isOpen, openPanel]); const result = useQuery({ query: getUserByEmailQuery, diff --git a/packages/frontend/admin/src/modules/accounts/components/edit-panel.tsx b/packages/frontend/admin/src/modules/accounts/components/edit-panel.tsx deleted file mode 100644 index 0177968c3a..0000000000 --- a/packages/frontend/admin/src/modules/accounts/components/edit-panel.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Button } from '@affine/admin/components/ui/button'; -import { Input } from '@affine/admin/components/ui/input'; -import { Label } from '@affine/admin/components/ui/label'; -import { Separator } from '@affine/admin/components/ui/separator'; -import { Switch } from '@affine/admin/components/ui/switch'; -import { FeatureType } from '@affine/graphql'; -import { CheckIcon, ChevronRightIcon, XIcon } from 'lucide-react'; -import { useCallback, useEffect, useState } from 'react'; - -import { useRightPanel } from '../../layout'; -import type { User } from '../schema'; -import { useUserManagement } from './use-user-management'; - -interface EditPanelProps { - user: User; - onResetPassword: () => void; - onDeleteAccount: () => void; -} - -export function EditPanel({ - user, - onResetPassword, - onDeleteAccount, -}: EditPanelProps) { - const { closePanel } = useRightPanel(); - const [name, setName] = useState(user.name); - const [email, setEmail] = useState(user.email); - const [features, setFeatures] = useState(user.features); - const { updateUser } = useUserManagement(); - - const disableSave = - name === user.name && email === user.email && features === user.features; - - const onConfirm = useCallback(() => { - updateUser({ - userId: user.id, - name, - email, - features, - callback: closePanel, - }); - }, [closePanel, email, features, name, updateUser, user.id]); - - const onEarlyAccessChange = useCallback( - (checked: boolean) => { - if (checked) { - setFeatures([...features, FeatureType.AIEarlyAccess]); - } else { - setFeatures(features.filter(f => f !== FeatureType.AIEarlyAccess)); - } - }, - [features] - ); - - const onAdminChange = useCallback( - (checked: boolean) => { - if (checked) { - setFeatures([...features, FeatureType.Admin]); - } else { - setFeatures(features.filter(f => f !== FeatureType.Admin)); - } - }, - [features] - ); - - useEffect(() => { - setName(user.name); - setEmail(user.email); - setFeatures(user.features); - }, [user]); - - return ( -
-
- - Edit Account - -
- -
-
-
- - setName(e.target.value)} - /> -
- -
- - setEmail(e.target.value)} - /> -
-
- -
- - - -
- -
-
- ); -} diff --git a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts index 19a586bb1b..cfe43dc7bd 100644 --- a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts +++ b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts @@ -4,161 +4,103 @@ import { useMutation, } from '@affine/core/hooks/use-mutation'; import { - addToAdminMutation, - addToEarlyAccessMutation, createChangePasswordUrlMutation, createUserMutation, deleteUserMutation, - EarlyAccessType, - FeatureType, listUsersQuery, - removeAdminMutation, - removeEarlyAccessMutation, + updateAccountFeaturesMutation, updateAccountMutation, } from '@affine/graphql'; import { useCallback, useMemo, useState } from 'react'; import { toast } from 'sonner'; +import type { UserInput } from '../schema'; + export const useCreateUser = () => { - const { trigger: createUser } = useMutation({ + const { + trigger: createAccount, + isMutating: creating, + error, + } = useMutation({ mutation: createUserMutation, }); - const { trigger: addToEarlyAccess } = useMutation({ - mutation: addToEarlyAccessMutation, - }); - - const { trigger: addToAdmin } = useMutation({ - mutation: addToAdminMutation, + const { trigger: updateAccountFeatures } = useMutation({ + mutation: updateAccountFeaturesMutation, }); const revalidate = useMutateQueryResource(); - const updateFeatures = useCallback( - (email: string, features: FeatureType[]) => { - const shouldAddToAdmin = features.includes(FeatureType.Admin); - const shouldAddToAIEarlyAccess = features.includes( - FeatureType.AIEarlyAccess - ); - - return Promise.all([ - shouldAddToAdmin && addToAdmin({ email }), - shouldAddToAIEarlyAccess && - addToEarlyAccess({ email, type: EarlyAccessType.AI }), - ]); - }, - [addToAdmin, addToEarlyAccess] - ); - const create = useAsyncCallback( - async ({ - name, - email, - password, - features, - callback, - }: { - name: string; - email: string; - password: string; - features: FeatureType[]; - callback?: () => void; - }) => { - await createUser({ - input: { - name, - email, - password, - }, - }) - .then(async () => { - await updateFeatures(email, features); - await revalidate(listUsersQuery); - toast('User created successfully'); - callback?.(); - }) - .catch(e => { - toast(e.message); - console.error(e); + async ({ name, email, features }: UserInput) => { + try { + const account = await createAccount({ + input: { + name, + email, + }, }); + + await updateAccountFeatures({ + userId: account.createUser.id, + features, + }); + await revalidate(listUsersQuery); + toast('Account updated successfully'); + } catch (e) { + toast.error('Failed to update account: ' + (e as Error).message); + } }, - [createUser, revalidate, updateFeatures] + [createAccount, revalidate] ); - return create; + return { creating: creating || !!error, create }; }; -interface UpdateUserProps { - userId: string; - name: string; - email: string; - features: FeatureType[]; - callback?: () => void; -} - export const useUpdateUser = () => { - const { trigger: updateAccount } = useMutation({ + const { + trigger: updateAccount, + isMutating: updating, + error, + } = useMutation({ mutation: updateAccountMutation, }); - const { trigger: addToEarlyAccess } = useMutation({ - mutation: addToEarlyAccessMutation, - }); - - const { trigger: removeEarlyAccess } = useMutation({ - mutation: removeEarlyAccessMutation, - }); - - const { trigger: addToAdmin } = useMutation({ - mutation: addToAdminMutation, - }); - - const { trigger: removeAdmin } = useMutation({ - mutation: removeAdminMutation, + const { trigger: updateAccountFeatures } = useMutation({ + mutation: updateAccountFeaturesMutation, }); const revalidate = useMutateQueryResource(); - const updateFeatures = useCallback( - ({ email, features }: { email: string; features: FeatureType[] }) => { - const shoutAddToAdmin = features.includes(FeatureType.Admin); - const shoutAddToAIEarlyAccess = features.includes( - FeatureType.AIEarlyAccess - ); - - return Promise.all([ - shoutAddToAdmin ? addToAdmin({ email }) : removeAdmin({ email }), - shoutAddToAIEarlyAccess - ? addToEarlyAccess({ email, type: EarlyAccessType.AI }) - : removeEarlyAccess({ email, type: EarlyAccessType.AI }), - ]); - }, - [addToAdmin, addToEarlyAccess, removeAdmin, removeEarlyAccess] - ); - const update = useAsyncCallback( - async ({ userId, name, email, features, callback }: UpdateUserProps) => { - updateAccount({ - id: userId, - input: { - name, - email, - }, - }) - .then(async () => { - await updateFeatures({ email, features }); - await revalidate(listUsersQuery); - toast('Account updated successfully'); - callback?.(); - }) - .catch(e => { - toast.error('Failed to update account: ' + e.message); + async ({ + userId, + name, + email, + features, + }: UserInput & { userId: string }) => { + try { + await updateAccount({ + id: userId, + input: { + name, + email, + }, }); + await updateAccountFeatures({ + userId, + features, + }); + await revalidate(listUsersQuery); + toast('Account updated successfully'); + } catch (e) { + toast.error('Failed to update account: ' + (e as Error).message); + } }, - [revalidate, updateAccount, updateFeatures] + [revalidate, updateAccount] ); - return update; + return { updating: updating || !!error, update }; }; export const useResetUserPassword = () => { @@ -217,20 +159,3 @@ export const useDeleteUser = () => { return deleteById; }; - -export const useUserManagement = () => { - const createUser = useCreateUser(); - const updateUser = useUpdateUser(); - const deleteUser = useDeleteUser(); - const { resetPasswordLink, onResetPassword } = useResetUserPassword(); - - return useMemo(() => { - return { - createUser, - updateUser, - deleteUser, - resetPasswordLink, - onResetPassword, - }; - }, [createUser, deleteUser, onResetPassword, resetPasswordLink, updateUser]); -}; diff --git a/packages/frontend/admin/src/modules/accounts/components/user-form.tsx b/packages/frontend/admin/src/modules/accounts/components/user-form.tsx new file mode 100644 index 0000000000..d0cf9dfbcf --- /dev/null +++ b/packages/frontend/admin/src/modules/accounts/components/user-form.tsx @@ -0,0 +1,288 @@ +import { Button } from '@affine/admin/components/ui/button'; +import { Input } from '@affine/admin/components/ui/input'; +import { Label } from '@affine/admin/components/ui/label'; +import { Separator } from '@affine/admin/components/ui/separator'; +import { Switch } from '@affine/admin/components/ui/switch'; +import type { FeatureType } from '@affine/graphql'; +import { CheckIcon, ChevronRightIcon, XIcon } from 'lucide-react'; +import type { ChangeEvent } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { useServerConfig } from '../../common'; +import type { UserInput, UserType } from '../schema'; +import { useCreateUser, useUpdateUser } from './use-user-management'; + +type UserFormProps = { + title: string; + defaultValue?: Partial; + onClose: () => void; + onConfirm: (user: UserInput) => void; + onValidate: (user: Partial) => boolean; + actions?: React.ReactNode; +}; + +function UserForm({ + title, + defaultValue, + onClose, + onConfirm, + onValidate, + actions, +}: UserFormProps) { + const serverConfig = useServerConfig(); + + const [changes, setChanges] = useState>({ + features: defaultValue?.features ?? [], + }); + + const setField = useCallback( + ( + field: K, + value: UserInput[K] | ((prev: UserInput[K] | undefined) => UserInput[K]) + ) => { + setChanges(changes => ({ + ...changes, + [field]: + typeof value === 'function' ? value(changes[field] as any) : value, + })); + }, + [] + ); + + const canSave = useMemo(() => { + return onValidate(changes); + }, [onValidate, changes]); + + const handleConfirm = useCallback(() => { + if (!canSave) { + return; + } + + // @ts-expect-error checked + onConfirm(changes); + }, [canSave, changes, onConfirm]); + + const onFeatureChanged = useCallback( + (feature: FeatureType, checked: boolean) => { + setField('features', (features = []) => { + if (checked) { + return [...features, feature]; + } + return features.filter(f => f !== feature); + }); + }, + [setField] + ); + + return ( +
+
+ + {title} + +
+ +
+
+ + + +
+ +
+ {serverConfig.availableUserFeatures.map((feature, i) => ( +
+ + {i < serverConfig.availableUserFeatures.length - 1 && ( + + )} +
+ ))} +
+ {actions} +
+
+ ); +} + +function ToggleItem({ + name, + checked, + onChange, +}: { + name: FeatureType; + checked: boolean; + onChange: (name: FeatureType, value: boolean) => void; +}) { + const onToggle = useCallback( + (checked: boolean) => { + onChange(name, checked); + }, + [name, onChange] + ); + + return ( + + ); +} + +function InputItem({ + label, + field, + value, + onChange, +}: { + label: string; + field: keyof UserInput; + value?: string; + onChange: (field: keyof UserInput, value: string) => void; +}) { + const onValueChange = useCallback( + (e: ChangeEvent) => { + onChange(field, e.target.value); + }, + [field, onChange] + ); + + return ( +
+ + +
+ ); +} + +const validateCreateUser = (user: Partial) => { + return !!user.name && !!user.email && !!user.features; +}; + +const validateUpdateUser = (user: Partial) => { + return !!user.name || !!user.email; +}; + +export function CreateUserForm({ onComplete }: { onComplete: () => void }) { + const { create, creating } = useCreateUser(); + useEffect(() => { + if (creating) { + return () => { + onComplete(); + }; + } + + return; + }, [creating, onComplete]); + return ( + + ); +} + +export function UpdateUserForm({ + user, + onResetPassword, + onDeleteAccount, + onComplete, +}: { + user: UserType; + onResetPassword: () => void; + onDeleteAccount: () => void; + onComplete: () => void; +}) { + const { update, updating } = useUpdateUser(); + + const onUpdateUser = useCallback( + (updates: UserInput) => { + update({ + ...updates, + userId: user.id, + }); + }, + [user, update] + ); + + useEffect(() => { + if (updating) { + return () => { + onComplete(); + }; + } + return; + }, [updating, onComplete]); + + return ( + + + + + } + /> + ); +} diff --git a/packages/frontend/admin/src/modules/accounts/index.tsx b/packages/frontend/admin/src/modules/accounts/index.tsx index 110c4e1ea2..27b5955e7b 100644 --- a/packages/frontend/admin/src/modules/accounts/index.tsx +++ b/packages/frontend/admin/src/modules/accounts/index.tsx @@ -37,6 +37,7 @@ export function AccountPage() { ; +export type UserType = ListUsersQuery['users'][0]; +export type UserInput = { + name: string; + email: string; + features: FeatureType[]; +}; diff --git a/packages/frontend/admin/src/modules/auth/index.tsx b/packages/frontend/admin/src/modules/auth/index.tsx index fc4dd256c5..c350d29b31 100644 --- a/packages/frontend/admin/src/modules/auth/index.tsx +++ b/packages/frontend/admin/src/modules/auth/index.tsx @@ -2,31 +2,21 @@ import { Button } from '@affine/admin/components/ui/button'; import { Input } from '@affine/admin/components/ui/input'; import { Label } from '@affine/admin/components/ui/label'; import { useMutateQueryResource } from '@affine/core/hooks/use-mutation'; -import { useQuery } from '@affine/core/hooks/use-query'; import { FeatureType, getCurrentUserFeaturesQuery, getUserFeaturesQuery, - serverConfigQuery, } from '@affine/graphql'; import { useCallback, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; +import { useCurrentUser, useServerConfig } from '../common'; import logo from './logo.svg'; export function Auth() { - const { - data: { currentUser }, - } = useQuery({ - query: getCurrentUserFeaturesQuery, - }); - - const { - data: { serverConfig }, - } = useQuery({ - query: serverConfigQuery, - }); + const currentUser = useCurrentUser(); + const serverConfig = useServerConfig(); const revalidate = useMutateQueryResource(); const emailRef = useRef(null); const passwordRef = useRef(null); diff --git a/packages/frontend/admin/src/modules/common.ts b/packages/frontend/admin/src/modules/common.ts new file mode 100644 index 0000000000..1c3b39c49f --- /dev/null +++ b/packages/frontend/admin/src/modules/common.ts @@ -0,0 +1,21 @@ +import { useQueryImmutable } from '@affine/core/hooks/use-query'; +import { + adminServerConfigQuery, + getCurrentUserFeaturesQuery, +} from '@affine/graphql'; + +export const useServerConfig = () => { + const { data } = useQueryImmutable({ + query: adminServerConfigQuery, + }); + + return data.serverConfig; +}; + +export const useCurrentUser = () => { + const { data } = useQueryImmutable({ + query: getCurrentUserFeaturesQuery, + }); + + return data.currentUser; +}; diff --git a/packages/frontend/admin/src/modules/config/index.tsx b/packages/frontend/admin/src/modules/config/index.tsx index 70f4578beb..480a7a66a9 100644 --- a/packages/frontend/admin/src/modules/config/index.tsx +++ b/packages/frontend/admin/src/modules/config/index.tsx @@ -6,7 +6,7 @@ import { } from '@affine/admin/components/ui/card'; import { ScrollArea } from '@affine/admin/components/ui/scroll-area'; import { Separator } from '@affine/admin/components/ui/separator'; -import { useQuery } from '@affine/core/hooks/use-query'; +import { useQueryImmutable } from '@affine/core/hooks/use-query'; import { getServerServiceConfigsQuery } from '@affine/graphql'; import { Layout } from '../layout'; @@ -171,7 +171,7 @@ const MailerCard = ({ mailerConfig }: { mailerConfig?: MailerConfig }) => { }; export function ServerServiceConfig() { - const { data } = useQuery({ + const { data } = useQueryImmutable({ query: getServerServiceConfigsQuery, }); const server = data.serverServiceConfigs.find( diff --git a/packages/frontend/admin/src/modules/layout.tsx b/packages/frontend/admin/src/modules/layout.tsx index aa361ba6ae..d2a0ba9b0d 100644 --- a/packages/frontend/admin/src/modules/layout.tsx +++ b/packages/frontend/admin/src/modules/layout.tsx @@ -7,11 +7,7 @@ import { Separator } from '@affine/admin/components/ui/separator'; import { TooltipProvider } from '@affine/admin/components/ui/tooltip'; import { cn } from '@affine/admin/utils'; import { useQuery } from '@affine/core/hooks/use-query'; -import { - FeatureType, - getCurrentUserFeaturesQuery, - serverConfigQuery, -} from '@affine/graphql'; +import { FeatureType, getCurrentUserFeaturesQuery } from '@affine/graphql'; import { AlignJustifyIcon } from 'lucide-react'; import type { ReactNode, RefObject } from 'react'; import { @@ -36,6 +32,7 @@ import { SheetTrigger, } from '../components/ui/sheet'; import { Logo } from './accounts/components/logo'; +import { useServerConfig } from './common'; import { NavContext } from './nav/context'; import { Nav } from './nav/nav'; @@ -85,6 +82,13 @@ export function useMediaQuery(query: string) { } export function Layout({ content }: LayoutProps) { + const serverConfig = useServerConfig(); + const { + data: { currentUser }, + } = useQuery({ + query: getCurrentUserFeaturesQuery, + }); + const [rightPanelContent, setRightPanelContent] = useState(null); const [open, setOpen] = useState(false); const rightPanelRef = useRef(null); @@ -122,16 +126,6 @@ export function Layout({ content }: LayoutProps) { [closePanel, openPanel] ); - const { - data: { serverConfig }, - } = useQuery({ - query: serverConfigQuery, - }); - const { - data: { currentUser }, - } = useQuery({ - query: getCurrentUserFeaturesQuery, - }); const navigate = useNavigate(); useEffect(() => { diff --git a/packages/frontend/admin/src/modules/nav/user-dropdown.tsx b/packages/frontend/admin/src/modules/nav/user-dropdown.tsx index 523b4fd71e..dedbda1d86 100644 --- a/packages/frontend/admin/src/modules/nav/user-dropdown.tsx +++ b/packages/frontend/admin/src/modules/nav/user-dropdown.tsx @@ -12,29 +12,17 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@affine/admin/components/ui/dropdown-menu'; -import { useQuery } from '@affine/core/hooks/use-query'; -import { - FeatureType, - getCurrentUserFeaturesQuery, - serverConfigQuery, -} from '@affine/graphql'; +import { FeatureType } from '@affine/graphql'; import { CircleUser, MoreVertical } from 'lucide-react'; import { useCallback, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; -export function UserDropdown() { - const { - data: { currentUser }, - } = useQuery({ - query: getCurrentUserFeaturesQuery, - }); +import { useCurrentUser, useServerConfig } from '../common'; - const { - data: { serverConfig }, - } = useQuery({ - query: serverConfigQuery, - }); +export function UserDropdown() { + const currentUser = useCurrentUser(); + const serverConfig = useServerConfig(); const navigate = useNavigate(); diff --git a/packages/frontend/admin/src/modules/setup/form.tsx b/packages/frontend/admin/src/modules/setup/form.tsx index daaa385ef8..7dd696fe02 100644 --- a/packages/frontend/admin/src/modules/setup/form.tsx +++ b/packages/frontend/admin/src/modules/setup/form.tsx @@ -7,12 +7,12 @@ import { } from '@affine/admin/components/ui/carousel'; import { validateEmailAndPassword } from '@affine/admin/utils'; import { useMutateQueryResource } from '@affine/core/hooks/use-mutation'; -import { useQuery } from '@affine/core/hooks/use-query'; import { serverConfigQuery } from '@affine/graphql'; import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; +import { useServerConfig } from '../common'; import { CreateAdmin } from './create-admin'; export enum CarouselSteps { @@ -72,10 +72,8 @@ export const Form = () => { const [invalidEmail, setInvalidEmail] = useState(false); const [invalidPassword, setInvalidPassword] = useState(false); - const { data } = useQuery({ - query: serverConfigQuery, - }); - const passwordLimits = data.serverConfig.credentialsRequirement.password; + const serverConfig = useServerConfig(); + const passwordLimits = serverConfig.credentialsRequirement.password; const isCreateAdminStep = current - 1 === CarouselSteps.CreateAdmin; @@ -95,7 +93,7 @@ export const Form = () => { api.on('select', () => { setCurrent(api.selectedScrollSnap() + 1); }); - }, [api, data.serverConfig.initialized, navigate]); + }, [api, serverConfig.initialized, navigate]); const createAdmin = useCallback(async () => { try { @@ -170,14 +168,14 @@ export const Form = () => { const onPrevious = useCallback(() => { if (current === count) { - if (data.serverConfig.initialized === true) { + if (serverConfig.initialized === true) { return navigate('/admin', { replace: true }); } toast.error('Goto Admin Panel failed, please try again.'); return; } api?.scrollPrev(); - }, [api, count, current, data.serverConfig.initialized, navigate]); + }, [api, count, current, serverConfig.initialized, navigate]); return (
diff --git a/packages/frontend/core/src/hooks/use-mutation.ts b/packages/frontend/core/src/hooks/use-mutation.ts index 58860b2fe6..09e1194fc7 100644 --- a/packages/frontend/core/src/hooks/use-mutation.ts +++ b/packages/frontend/core/src/hooks/use-mutation.ts @@ -35,7 +35,7 @@ export function useMutation( config?: Omit< SWRMutationConfiguration< QueryResponse, - GraphQLError | GraphQLError[], + GraphQLError, K, QueryVariables >, @@ -43,7 +43,7 @@ export function useMutation( > ): SWRMutationResponse< QueryResponse, - GraphQLError | GraphQLError[], + GraphQLError, K, QueryVariables >; diff --git a/packages/frontend/core/src/hooks/use-query.ts b/packages/frontend/core/src/hooks/use-query.ts index ac39aa82f1..421a4ff2a3 100644 --- a/packages/frontend/core/src/hooks/use-query.ts +++ b/packages/frontend/core/src/hooks/use-query.ts @@ -32,16 +32,12 @@ import useSWRInfinite from 'swr/infinite'; type useQueryFn = ( options?: QueryOptions, config?: Omit< - SWRConfiguration< - QueryResponse, - GraphQLError | GraphQLError[], - typeof fetcher - >, + SWRConfiguration, GraphQLError, typeof fetcher>, 'fetcher' > ) => SWRResponse< QueryResponse, - GraphQLError | GraphQLError[], + GraphQLError, { suspense: true; } diff --git a/packages/frontend/graphql/src/graphql/add-admin.gql b/packages/frontend/graphql/src/graphql/add-admin.gql deleted file mode 100644 index 5b8fc69711..0000000000 --- a/packages/frontend/graphql/src/graphql/add-admin.gql +++ /dev/null @@ -1,3 +0,0 @@ -mutation addToAdmin($email: String!) { - addAdminister(email: $email) -} diff --git a/packages/frontend/graphql/src/graphql/admin-server-config.gql b/packages/frontend/graphql/src/graphql/admin-server-config.gql new file mode 100644 index 0000000000..f49ee978a4 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/admin-server-config.gql @@ -0,0 +1,17 @@ +#import './fragments/password-limits.gql' +#import './fragments/credentials-requirement.gql' + +query adminServerConfig { + serverConfig { + version + baseUrl + name + features + type + initialized + credentialsRequirement { + ...CredentialsRequirement + } + availableUserFeatures + } +} diff --git a/packages/frontend/graphql/src/graphql/early-access-add.gql b/packages/frontend/graphql/src/graphql/early-access-add.gql deleted file mode 100644 index 47680a48c3..0000000000 --- a/packages/frontend/graphql/src/graphql/early-access-add.gql +++ /dev/null @@ -1,3 +0,0 @@ -mutation addToEarlyAccess($email: String!, $type: EarlyAccessType!) { - addToEarlyAccess(email: $email, type: $type) -} diff --git a/packages/frontend/graphql/src/graphql/early-access-list.gql b/packages/frontend/graphql/src/graphql/early-access-list.gql deleted file mode 100644 index 1b87d67fa6..0000000000 --- a/packages/frontend/graphql/src/graphql/early-access-list.gql +++ /dev/null @@ -1,16 +0,0 @@ -query earlyAccessUsers { - earlyAccessUsers { - id - name - email - avatarUrl - emailVerified - subscription { - plan - recurring - status - start - end - } - } -} diff --git a/packages/frontend/graphql/src/graphql/early-access-remove.gql b/packages/frontend/graphql/src/graphql/early-access-remove.gql deleted file mode 100644 index eb99a5dcb0..0000000000 --- a/packages/frontend/graphql/src/graphql/early-access-remove.gql +++ /dev/null @@ -1,3 +0,0 @@ -mutation removeEarlyAccess($email: String!, $type: EarlyAccessType!) { - removeEarlyAccess(email: $email, type: $type) -} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index af2f093a36..295bf20483 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -18,15 +18,27 @@ fragment CredentialsRequirement on CredentialsRequirementType { ...PasswordLimits } }` -export const addToAdminMutation = { - id: 'addToAdminMutation' as const, - operationName: 'addToAdmin', - definitionName: 'addAdminister', +export const adminServerConfigQuery = { + id: 'adminServerConfigQuery' as const, + operationName: 'adminServerConfig', + definitionName: 'serverConfig', containsFile: false, query: ` -mutation addToAdmin($email: String!) { - addAdminister(email: $email) -}`, +query adminServerConfig { + serverConfig { + version + baseUrl + name + features + type + initialized + credentialsRequirement { + ...CredentialsRequirement + } + availableUserFeatures + } +}${passwordLimitsFragment} +${credentialsRequirementFragment}`, }; export const deleteBlobMutation = { @@ -254,52 +266,6 @@ mutation deleteWorkspace($id: String!) { }`, }; -export const addToEarlyAccessMutation = { - id: 'addToEarlyAccessMutation' as const, - operationName: 'addToEarlyAccess', - definitionName: 'addToEarlyAccess', - containsFile: false, - query: ` -mutation addToEarlyAccess($email: String!, $type: EarlyAccessType!) { - addToEarlyAccess(email: $email, type: $type) -}`, -}; - -export const earlyAccessUsersQuery = { - id: 'earlyAccessUsersQuery' as const, - operationName: 'earlyAccessUsers', - definitionName: 'earlyAccessUsers', - containsFile: false, - query: ` -query earlyAccessUsers { - earlyAccessUsers { - id - name - email - avatarUrl - emailVerified - subscription { - plan - recurring - status - start - end - } - } -}`, -}; - -export const removeEarlyAccessMutation = { - id: 'removeEarlyAccessMutation' as const, - operationName: 'removeEarlyAccess', - definitionName: 'removeEarlyAccess', - containsFile: false, - query: ` -mutation removeEarlyAccess($email: String!, $type: EarlyAccessType!) { - removeEarlyAccess(email: $email, type: $type) -}`, -}; - export const forkCopilotSessionMutation = { id: 'forkCopilotSessionMutation' as const, operationName: 'forkCopilotSession', @@ -803,15 +769,6 @@ query listUsers($filter: ListUserInput!) { hasPassword emailVerified avatarUrl - quota { - humanReadable { - blobLimit - historyPeriod - memberLimit - name - storageQuota - } - } } }`, }; @@ -889,17 +846,6 @@ mutation recoverDoc($workspaceId: String!, $docId: String!, $timestamp: DateTime }`, }; -export const removeAdminMutation = { - id: 'removeAdminMutation' as const, - operationName: 'removeAdmin', - definitionName: 'removeAdminister', - containsFile: false, - query: ` -mutation removeAdmin($email: String!) { - removeAdminister(email: $email) -}`, -}; - export const removeAvatarMutation = { id: 'removeAvatarMutation' as const, operationName: 'removeAvatar', @@ -1024,7 +970,6 @@ query serverConfig { name features type - initialized credentialsRequirement { ...CredentialsRequirement } @@ -1069,6 +1014,17 @@ query subscription { }`, }; +export const updateAccountFeaturesMutation = { + id: 'updateAccountFeaturesMutation' as const, + operationName: 'updateAccountFeatures', + definitionName: 'updateUserFeatures', + containsFile: false, + query: ` +mutation updateAccountFeatures($userId: String!, $features: [FeatureType!]!) { + updateUserFeatures(id: $userId, features: $features) +}`, +}; + export const updateAccountMutation = { id: 'updateAccountMutation' as const, operationName: 'updateAccount', diff --git a/packages/frontend/graphql/src/graphql/list-users.gql b/packages/frontend/graphql/src/graphql/list-users.gql index 2fe496283d..9e96d08558 100644 --- a/packages/frontend/graphql/src/graphql/list-users.gql +++ b/packages/frontend/graphql/src/graphql/list-users.gql @@ -7,14 +7,5 @@ query listUsers($filter: ListUserInput!) { hasPassword emailVerified avatarUrl - quota { - humanReadable { - blobLimit - historyPeriod - memberLimit - name - storageQuota - } - } } } diff --git a/packages/frontend/graphql/src/graphql/remove-admin.gql b/packages/frontend/graphql/src/graphql/remove-admin.gql deleted file mode 100644 index 315c9a8ae6..0000000000 --- a/packages/frontend/graphql/src/graphql/remove-admin.gql +++ /dev/null @@ -1,3 +0,0 @@ -mutation removeAdmin($email: String!) { - removeAdminister(email: $email) -} diff --git a/packages/frontend/graphql/src/graphql/server-config.gql b/packages/frontend/graphql/src/graphql/server-config.gql index 1987157dbc..ef17ef188a 100644 --- a/packages/frontend/graphql/src/graphql/server-config.gql +++ b/packages/frontend/graphql/src/graphql/server-config.gql @@ -8,7 +8,6 @@ query serverConfig { name features type - initialized credentialsRequirement { ...CredentialsRequirement } diff --git a/packages/frontend/graphql/src/graphql/update-account-features.gql b/packages/frontend/graphql/src/graphql/update-account-features.gql new file mode 100644 index 0000000000..4d8590e5f7 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/update-account-features.gql @@ -0,0 +1,3 @@ +mutation updateAccountFeatures($userId: String!, $features: [FeatureType!]!) { + updateUserFeatures(id: $userId, features: $features) +} diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index b564aad99d..57a2b29fb8 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -200,7 +200,6 @@ export interface CreateCopilotPromptInput { export interface CreateUserInput { email: Scalars['String']['input']; name: InputMaybe; - password: InputMaybe; } export interface CredentialsRequirementType { @@ -245,11 +244,6 @@ export interface DocNotFoundDataType { workspaceId: Scalars['String']['output']; } -export enum EarlyAccessType { - AI = 'AI', - App = 'App', -} - export type ErrorDataUnion = | BlobNotFoundDataType | CopilotMessageNotFoundDataType @@ -280,6 +274,8 @@ export enum ErrorNames { AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED', BLOB_NOT_FOUND = 'BLOB_NOT_FOUND', BLOB_QUOTA_EXCEEDED = 'BLOB_QUOTA_EXCEEDED', + CANNOT_DELETE_ALL_ADMIN_ACCOUNT = 'CANNOT_DELETE_ALL_ADMIN_ACCOUNT', + CANNOT_DELETE_OWN_ACCOUNT = 'CANNOT_DELETE_OWN_ACCOUNT', CANT_CHANGE_WORKSPACE_OWNER = 'CANT_CHANGE_WORKSPACE_OWNER', CANT_UPDATE_LIFETIME_SUBSCRIPTION = 'CANT_UPDATE_LIFETIME_SUBSCRIPTION', COPILOT_ACTION_TAKEN = 'COPILOT_ACTION_TAKEN', @@ -455,7 +451,7 @@ export interface ListUserInput { export interface ManageUserInput { /** User email */ - email: Scalars['String']['input']; + email: InputMaybe; /** User name */ name: InputMaybe; } @@ -468,8 +464,6 @@ export interface MissingOauthQueryParameterDataType { export interface Mutation { __typename?: 'Mutation'; acceptInviteById: Scalars['Boolean']['output']; - addAdminister: Scalars['Boolean']['output']; - addToEarlyAccess: Scalars['Int']['output']; addWorkspaceFeature: Scalars['Int']['output']; cancelSubscription: UserSubscription; changeEmail: UserType; @@ -503,10 +497,8 @@ export interface Mutation { leaveWorkspace: Scalars['Boolean']['output']; publishPage: WorkspacePage; recoverDoc: Scalars['DateTime']['output']; - removeAdminister: Scalars['Boolean']['output']; /** Remove user avatar */ removeAvatar: RemoveAvatar; - removeEarlyAccess: Scalars['Int']['output']; removeWorkspaceFeature: Scalars['Int']['output']; resumeSubscription: UserSubscription; revoke: Scalars['Boolean']['output']; @@ -532,6 +524,8 @@ export interface Mutation { updateSubscriptionRecurring: UserSubscription; /** Update a user */ updateUser: UserType; + /** update user enabled feature */ + updateUserFeatures: Array; /** Update workspace */ updateWorkspace: WorkspaceType; /** Upload user avatar */ @@ -545,15 +539,6 @@ export interface MutationAcceptInviteByIdArgs { workspaceId: Scalars['String']['input']; } -export interface MutationAddAdministerArgs { - email: Scalars['String']['input']; -} - -export interface MutationAddToEarlyAccessArgs { - email: Scalars['String']['input']; - type: EarlyAccessType; -} - export interface MutationAddWorkspaceFeatureArgs { feature: FeatureType; workspaceId: Scalars['String']['input']; @@ -649,15 +634,6 @@ export interface MutationRecoverDocArgs { workspaceId: Scalars['String']['input']; } -export interface MutationRemoveAdministerArgs { - email: Scalars['String']['input']; -} - -export interface MutationRemoveEarlyAccessArgs { - email: Scalars['String']['input']; - type: EarlyAccessType; -} - export interface MutationRemoveWorkspaceFeatureArgs { feature: FeatureType; workspaceId: Scalars['String']['input']; @@ -753,6 +729,11 @@ export interface MutationUpdateUserArgs { input: ManageUserInput; } +export interface MutationUpdateUserFeaturesArgs { + features: Array; + id: Scalars['String']['input']; +} + export interface MutationUpdateWorkspaceArgs { input: UpdateWorkspaceInput; } @@ -804,7 +785,6 @@ export interface Query { collectAllBlobSizes: WorkspaceBlobSizes; /** Get current user */ currentUser: Maybe; - earlyAccessUsers: Array; error: ErrorDataUnion; /** send workspace invitation */ getInviteInfo: InvitationType; @@ -932,6 +912,8 @@ export interface SameSubscriptionRecurringDataType { export interface ServerConfigType { __typename?: 'ServerConfigType'; + /** Features for user that can be configured */ + availableUserFeatures: Array; /** server base url */ baseUrl: Scalars['String']['output']; /** credentials requirement */ @@ -1254,13 +1236,28 @@ export interface TokenType { token: Scalars['String']['output']; } -export type AddToAdminMutationVariables = Exact<{ - email: Scalars['String']['input']; -}>; +export type AdminServerConfigQueryVariables = Exact<{ [key: string]: never }>; -export type AddToAdminMutation = { - __typename?: 'Mutation'; - addAdminister: boolean; +export type AdminServerConfigQuery = { + __typename?: 'Query'; + serverConfig: { + __typename?: 'ServerConfigType'; + version: string; + baseUrl: string; + name: string; + features: Array; + type: ServerDeploymentType; + initialized: boolean; + availableUserFeatures: Array; + credentialsRequirement: { + __typename?: 'CredentialsRequirementType'; + password: { + __typename?: 'PasswordLimitsType'; + minLength: number; + maxLength: number; + }; + }; + }; }; export type DeleteBlobMutationVariables = Exact<{ @@ -1440,48 +1437,6 @@ export type DeleteWorkspaceMutation = { deleteWorkspace: boolean; }; -export type AddToEarlyAccessMutationVariables = Exact<{ - email: Scalars['String']['input']; - type: EarlyAccessType; -}>; - -export type AddToEarlyAccessMutation = { - __typename?: 'Mutation'; - addToEarlyAccess: number; -}; - -export type EarlyAccessUsersQueryVariables = Exact<{ [key: string]: never }>; - -export type EarlyAccessUsersQuery = { - __typename?: 'Query'; - earlyAccessUsers: Array<{ - __typename?: 'UserType'; - id: string; - name: string; - email: string; - avatarUrl: string | null; - emailVerified: boolean; - subscription: { - __typename?: 'UserSubscription'; - plan: SubscriptionPlan; - recurring: SubscriptionRecurring; - status: SubscriptionStatus; - start: string; - end: string | null; - } | null; - }>; -}; - -export type RemoveEarlyAccessMutationVariables = Exact<{ - email: Scalars['String']['input']; - type: EarlyAccessType; -}>; - -export type RemoveEarlyAccessMutation = { - __typename?: 'Mutation'; - removeEarlyAccess: number; -}; - export type ForkCopilotSessionMutationVariables = Exact<{ options: ForkChatSessionInput; }>; @@ -1956,17 +1911,6 @@ export type ListUsersQuery = { hasPassword: boolean | null; emailVerified: boolean; avatarUrl: string | null; - quota: { - __typename?: 'UserQuota'; - humanReadable: { - __typename?: 'UserQuotaHumanReadable'; - blobLimit: string; - historyPeriod: string; - memberLimit: string; - name: string; - storageQuota: string; - }; - } | null; }>; }; @@ -2038,15 +1982,6 @@ export type RecoverDocMutation = { recoverDoc: string; }; -export type RemoveAdminMutationVariables = Exact<{ - email: Scalars['String']['input']; -}>; - -export type RemoveAdminMutation = { - __typename?: 'Mutation'; - removeAdminister: boolean; -}; - export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>; export type RemoveAvatarMutation = { @@ -2154,7 +2089,6 @@ export type ServerConfigQuery = { name: string; features: Array; type: ServerDeploymentType; - initialized: boolean; credentialsRequirement: { __typename?: 'CredentialsRequirementType'; password: { @@ -2197,6 +2131,16 @@ export type SubscriptionQuery = { } | null; }; +export type UpdateAccountFeaturesMutationVariables = Exact<{ + userId: Scalars['String']['input']; + features: Array | FeatureType; +}>; + +export type UpdateAccountFeaturesMutation = { + __typename?: 'Mutation'; + updateUserFeatures: Array; +}; + export type UpdateAccountMutationVariables = Exact<{ id: Scalars['String']['input']; input: ManageUserInput; @@ -2422,6 +2366,11 @@ export type WorkspaceQuotaQuery = { }; export type Queries = + | { + name: 'adminServerConfigQuery'; + variables: AdminServerConfigQueryVariables; + response: AdminServerConfigQuery; + } | { name: 'listBlobsQuery'; variables: ListBlobsQueryVariables; @@ -2432,11 +2381,6 @@ export type Queries = variables: CopilotQuotaQueryVariables; response: CopilotQuotaQuery; } - | { - name: 'earlyAccessUsersQuery'; - variables: EarlyAccessUsersQueryVariables; - response: EarlyAccessUsersQuery; - } | { name: 'getCopilotHistoriesQuery'; variables: GetCopilotHistoriesQueryVariables; @@ -2614,11 +2558,6 @@ export type Queries = }; export type Mutations = - | { - name: 'addToAdminMutation'; - variables: AddToAdminMutationVariables; - response: AddToAdminMutation; - } | { name: 'deleteBlobMutation'; variables: DeleteBlobMutationVariables; @@ -2699,16 +2638,6 @@ export type Mutations = variables: DeleteWorkspaceMutationVariables; response: DeleteWorkspaceMutation; } - | { - name: 'addToEarlyAccessMutation'; - variables: AddToEarlyAccessMutationVariables; - response: AddToEarlyAccessMutation; - } - | { - name: 'removeEarlyAccessMutation'; - variables: RemoveEarlyAccessMutationVariables; - response: RemoveEarlyAccessMutation; - } | { name: 'forkCopilotSessionMutation'; variables: ForkCopilotSessionMutationVariables; @@ -2729,11 +2658,6 @@ export type Mutations = variables: RecoverDocMutationVariables; response: RecoverDocMutation; } - | { - name: 'removeAdminMutation'; - variables: RemoveAdminMutationVariables; - response: RemoveAdminMutation; - } | { name: 'removeAvatarMutation'; variables: RemoveAvatarMutationVariables; @@ -2784,6 +2708,11 @@ export type Mutations = variables: SetWorkspacePublicByIdMutationVariables; response: SetWorkspacePublicByIdMutation; } + | { + name: 'updateAccountFeaturesMutation'; + variables: UpdateAccountFeaturesMutationVariables; + response: UpdateAccountFeaturesMutation; + } | { name: 'updateAccountMutation'; variables: UpdateAccountMutationVariables;