mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
fix(admin): organize admin panel (#7840)
This commit is contained in:
@@ -155,7 +155,7 @@ function buildAppModule() {
|
|||||||
.useIf(config => config.flavor.sync, WebSocketModule)
|
.useIf(config => config.flavor.sync, WebSocketModule)
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
.use(AuthModule)
|
.use(UserModule, AuthModule)
|
||||||
|
|
||||||
// business modules
|
// business modules
|
||||||
.use(DocModule)
|
.use(DocModule)
|
||||||
@@ -169,7 +169,6 @@ function buildAppModule() {
|
|||||||
ServerConfigModule,
|
ServerConfigModule,
|
||||||
GqlModule,
|
GqlModule,
|
||||||
StorageModule,
|
StorageModule,
|
||||||
UserModule,
|
|
||||||
WorkspaceModule,
|
WorkspaceModule,
|
||||||
FeatureModule,
|
FeatureModule,
|
||||||
QuotaModule
|
QuotaModule
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Cron, CronExpression } from '@nestjs/schedule';
|
|||||||
import type { User, UserSession } from '@prisma/client';
|
import type { User, UserSession } from '@prisma/client';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import type { CookieOptions, Request, Response } from 'express';
|
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 { Config, EmailAlreadyUsed, MailService } from '../../fundamentals';
|
||||||
import { FeatureManagementService } from '../features/management';
|
import { FeatureManagementService } from '../features/management';
|
||||||
@@ -41,13 +41,11 @@ export function sessionUser(
|
|||||||
'id' | 'email' | 'avatarUrl' | 'name' | 'emailVerifiedAt'
|
'id' | 'email' | 'avatarUrl' | 'name' | 'emailVerifiedAt'
|
||||||
> & { password?: string | null }
|
> & { password?: string | null }
|
||||||
): CurrentUser {
|
): CurrentUser {
|
||||||
return assign(
|
// use pick to avoid unexpected fields
|
||||||
omit(user, 'password', 'registered', 'emailVerifiedAt', 'createdAt'),
|
return assign(pick(user, 'id', 'email', 'avatarUrl', 'name'), {
|
||||||
{
|
hasPassword: user.password !== null,
|
||||||
hasPassword: user.password !== null,
|
emailVerified: user.emailVerifiedAt !== null,
|
||||||
emailVerified: user.emailVerifiedAt !== null,
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ import {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServerConfigModule {}
|
export class ServerConfigModule {}
|
||||||
export { ADD_ENABLED_FEATURES, ServerConfigType } from './resolver';
|
export { ADD_ENABLED_FEATURES } from './server-feature';
|
||||||
export { ServerFeature } from './types';
|
export { ServerFeature } from './types';
|
||||||
|
|||||||
@@ -12,24 +12,14 @@ import {
|
|||||||
import { PrismaClient, RuntimeConfig, RuntimeConfigType } from '@prisma/client';
|
import { PrismaClient, RuntimeConfig, RuntimeConfigType } from '@prisma/client';
|
||||||
import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
|
import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
|
||||||
|
|
||||||
import { Config, DeploymentType, URLHelper } from '../../fundamentals';
|
import { Config, URLHelper } from '../../fundamentals';
|
||||||
import { Public } from '../auth';
|
import { Public } from '../auth';
|
||||||
import { Admin } from '../common';
|
import { Admin } from '../common';
|
||||||
|
import { FeatureType } from '../features';
|
||||||
|
import { AvailableUserFeatureConfig } from '../features/resolver';
|
||||||
import { ServerFlags } from './config';
|
import { ServerFlags } from './config';
|
||||||
import { ServerFeature } from './types';
|
import { ENABLED_FEATURES } from './server-feature';
|
||||||
|
import { ServerConfigType } from './types';
|
||||||
const ENABLED_FEATURES: Set<ServerFeature> = new Set();
|
|
||||||
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
|
|
||||||
ENABLED_FEATURES.add(feature);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerEnumType(ServerFeature, {
|
|
||||||
name: 'ServerFeature',
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEnumType(DeploymentType, {
|
|
||||||
name: 'ServerDeploymentType',
|
|
||||||
});
|
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class PasswordLimitsType {
|
export class PasswordLimitsType {
|
||||||
@@ -45,36 +35,6 @@ export class CredentialsRequirementType {
|
|||||||
password!: PasswordLimitsType;
|
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, {
|
registerEnumType(RuntimeConfigType, {
|
||||||
name: '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()
|
@ObjectType()
|
||||||
class ServerServiceConfig {
|
class ServerServiceConfig {
|
||||||
@Field()
|
@Field()
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ServerFeature } from './types';
|
||||||
|
|
||||||
|
export const ENABLED_FEATURES: Set<ServerFeature> = new Set();
|
||||||
|
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
|
||||||
|
ENABLED_FEATURES.add(feature);
|
||||||
|
}
|
||||||
|
export { ServerFeature };
|
||||||
@@ -1,5 +1,47 @@
|
|||||||
|
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { DeploymentType } from '../../fundamentals';
|
||||||
|
|
||||||
export enum ServerFeature {
|
export enum ServerFeature {
|
||||||
Copilot = 'copilot',
|
Copilot = 'copilot',
|
||||||
Payment = 'payment',
|
Payment = 'payment',
|
||||||
OAuth = 'oauth',
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { UserModule } from '../user';
|
import { UserModule } from '../user';
|
||||||
import { EarlyAccessType, FeatureManagementService } from './management';
|
import { EarlyAccessType, FeatureManagementService } from './management';
|
||||||
import { FeatureManagementResolver } from './resolver';
|
import {
|
||||||
|
AdminFeatureManagementResolver,
|
||||||
|
FeatureManagementResolver,
|
||||||
|
} from './resolver';
|
||||||
import { FeatureService } from './service';
|
import { FeatureService } from './service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +20,7 @@ import { FeatureService } from './service';
|
|||||||
FeatureService,
|
FeatureService,
|
||||||
FeatureManagementService,
|
FeatureManagementService,
|
||||||
FeatureManagementResolver,
|
FeatureManagementResolver,
|
||||||
|
AdminFeatureManagementResolver,
|
||||||
],
|
],
|
||||||
exports: [FeatureService, FeatureManagementService],
|
exports: [FeatureService, FeatureManagementService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ export class FeatureManagementService {
|
|||||||
return this.feature.addUserFeature(userId, FeatureType.Admin, 'Admin user');
|
return this.feature.addUserFeature(userId, FeatureType.Admin, 'Admin user');
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAdmin(userId: string) {
|
|
||||||
return this.feature.removeUserFeature(userId, FeatureType.Admin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== Early Access ========
|
// ======== Early Access ========
|
||||||
async addEarlyAccess(
|
async addEarlyAccess(
|
||||||
userId: string,
|
userId: string,
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
Context,
|
|
||||||
Int,
|
|
||||||
Mutation,
|
Mutation,
|
||||||
Parent,
|
Parent,
|
||||||
Query,
|
|
||||||
registerEnumType,
|
registerEnumType,
|
||||||
ResolveField,
|
ResolveField,
|
||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
|
import { difference } from 'lodash-es';
|
||||||
|
|
||||||
import { UserNotFound } from '../../fundamentals';
|
import { Config } from '../../fundamentals';
|
||||||
import { sessionUser } from '../auth/service';
|
|
||||||
import { Admin } from '../common';
|
import { Admin } from '../common';
|
||||||
import { UserService } from '../user/service';
|
|
||||||
import { UserType } from '../user/types';
|
import { UserType } from '../user/types';
|
||||||
import { EarlyAccessType, FeatureManagementService } from './management';
|
import { EarlyAccessType, FeatureManagementService } from './management';
|
||||||
|
import { FeatureService } from './service';
|
||||||
import { FeatureType } from './types';
|
import { FeatureType } from './types';
|
||||||
|
|
||||||
registerEnumType(EarlyAccessType, {
|
registerEnumType(EarlyAccessType, {
|
||||||
@@ -24,10 +21,7 @@ registerEnumType(EarlyAccessType, {
|
|||||||
|
|
||||||
@Resolver(() => UserType)
|
@Resolver(() => UserType)
|
||||||
export class FeatureManagementResolver {
|
export class FeatureManagementResolver {
|
||||||
constructor(
|
constructor(private readonly feature: FeatureManagementService) {}
|
||||||
private readonly users: UserService,
|
|
||||||
private readonly feature: FeatureManagementService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@ResolveField(() => [FeatureType], {
|
@ResolveField(() => [FeatureType], {
|
||||||
name: 'features',
|
name: 'features',
|
||||||
@@ -36,75 +30,48 @@ export class FeatureManagementResolver {
|
|||||||
async userFeatures(@Parent() user: UserType) {
|
async userFeatures(@Parent() user: UserType) {
|
||||||
return this.feature.getActivatedUserFeatures(user.id);
|
return this.feature.getActivatedUserFeatures(user.id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Admin()
|
export class AvailableUserFeatureConfig {
|
||||||
@Mutation(() => Int)
|
constructor(private readonly config: Config) {}
|
||||||
async addToEarlyAccess(
|
|
||||||
@Args('email') email: string,
|
|
||||||
@Args({ name: 'type', type: () => EarlyAccessType }) type: EarlyAccessType
|
|
||||||
): Promise<number> {
|
|
||||||
const user = await this.users.findUserByEmail(email);
|
|
||||||
if (user) {
|
|
||||||
return this.feature.addEarlyAccess(user.id, type);
|
|
||||||
} else {
|
|
||||||
const user = await this.users.createUser({
|
|
||||||
email,
|
|
||||||
registered: false,
|
|
||||||
});
|
|
||||||
return this.feature.addEarlyAccess(user.id, type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Admin()
|
async availableUserFeatures() {
|
||||||
@Mutation(() => Int)
|
return this.config.isSelfhosted
|
||||||
async removeEarlyAccess(
|
? [FeatureType.Admin]
|
||||||
@Args('email') email: string,
|
: [FeatureType.EarlyAccess, FeatureType.AIEarlyAccess, FeatureType.Admin];
|
||||||
@Args({ name: 'type', type: () => EarlyAccessType }) type: EarlyAccessType
|
}
|
||||||
): Promise<number> {
|
}
|
||||||
const user = await this.users.findUserByEmail(email);
|
|
||||||
if (!user) {
|
@Admin()
|
||||||
throw new UserNotFound();
|
@Resolver(() => Boolean)
|
||||||
}
|
export class AdminFeatureManagementResolver extends AvailableUserFeatureConfig {
|
||||||
return this.feature.removeEarlyAccess(user.id, type);
|
constructor(
|
||||||
}
|
config: Config,
|
||||||
|
private readonly feature: FeatureService
|
||||||
@Admin()
|
) {
|
||||||
@Query(() => [UserType])
|
super(config);
|
||||||
async earlyAccessUsers(
|
}
|
||||||
@Context() ctx: { isAdminQuery: boolean }
|
|
||||||
): Promise<UserType[]> {
|
@Mutation(() => [FeatureType], {
|
||||||
// allow query other user's subscription
|
description: 'update user enabled feature',
|
||||||
ctx.isAdminQuery = true;
|
})
|
||||||
return this.feature.listEarlyAccess().then(users => {
|
async updateUserFeatures(
|
||||||
return users.map(sessionUser);
|
@Args('id') id: string,
|
||||||
});
|
@Args({ name: 'features', type: () => [FeatureType] })
|
||||||
}
|
features: FeatureType[]
|
||||||
|
) {
|
||||||
@Admin()
|
const configurableFeatures = await this.availableUserFeatures();
|
||||||
@Mutation(() => Boolean)
|
|
||||||
async addAdminister(@Args('email') email: string): Promise<boolean> {
|
const removed = difference(configurableFeatures, features);
|
||||||
const user = await this.users.findUserByEmail(email);
|
await Promise.all(
|
||||||
|
features.map(feature =>
|
||||||
if (!user) {
|
this.feature.addUserFeature(id, feature, 'admin panel')
|
||||||
throw new UserNotFound();
|
)
|
||||||
}
|
);
|
||||||
|
await Promise.all(
|
||||||
await this.feature.addAdmin(user.id);
|
removed.map(feature => this.feature.removeUserFeature(id, feature))
|
||||||
|
);
|
||||||
return true;
|
|
||||||
}
|
return features;
|
||||||
|
|
||||||
@Admin()
|
|
||||||
@Mutation(() => Boolean)
|
|
||||||
async removeAdminister(@Args('email') email: string): Promise<boolean> {
|
|
||||||
const user = await this.users.findUserByEmail(email);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new UserNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.feature.removeAdmin(user.id);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
import { CannotDeleteAllAdminAccount } from '../../fundamentals';
|
||||||
import { WorkspaceType } from '../workspaces/types';
|
import { WorkspaceType } from '../workspaces/types';
|
||||||
import { FeatureConfigType, getFeature } from './feature';
|
import { FeatureConfigType, getFeature } from './feature';
|
||||||
import { FeatureKind, FeatureType } from './types';
|
import { FeatureKind, FeatureType } from './types';
|
||||||
@@ -81,6 +82,9 @@ export class FeatureService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeUserFeature(userId: string, feature: FeatureType) {
|
async removeUserFeature(userId: string, feature: FeatureType) {
|
||||||
|
if (feature === FeatureType.Admin) {
|
||||||
|
await this.ensureNotLastAdmin(userId);
|
||||||
|
}
|
||||||
return this.prisma.userFeature
|
return this.prisma.userFeature
|
||||||
.updateMany({
|
.updateMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -98,6 +102,20 @@ export class FeatureService {
|
|||||||
.then(r => r.count);
|
.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
|
* get user's features, will included inactivated features
|
||||||
* @param userId user id
|
* @param userId user id
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||||
import { isNil, omitBy } from 'lodash-es';
|
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 { CurrentUser } from '../auth/current-user';
|
||||||
import { Public } from '../auth/guard';
|
import { Public } from '../auth/guard';
|
||||||
import { sessionUser } from '../auth/service';
|
import { sessionUser } from '../auth/service';
|
||||||
@@ -162,9 +167,6 @@ class CreateUserInput {
|
|||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
name!: string | null;
|
name!: string | null;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
|
||||||
password!: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Admin()
|
@Admin()
|
||||||
@@ -244,7 +246,6 @@ export class UserManagementResolver {
|
|||||||
) {
|
) {
|
||||||
const { id } = await this.user.createUser({
|
const { id } = await this.user.createUser({
|
||||||
email: input.email,
|
email: input.email,
|
||||||
password: input.password,
|
|
||||||
registered: true,
|
registered: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -255,7 +256,13 @@ export class UserManagementResolver {
|
|||||||
@Mutation(() => DeleteAccount, {
|
@Mutation(() => DeleteAccount, {
|
||||||
description: 'Delete a user account',
|
description: 'Delete a user account',
|
||||||
})
|
})
|
||||||
async deleteUser(@Args('id') id: string): Promise<DeleteAccount> {
|
async deleteUser(
|
||||||
|
@CurrentUser() user: CurrentUser,
|
||||||
|
@Args('id') id: string
|
||||||
|
): Promise<DeleteAccount> {
|
||||||
|
if (user.id === id) {
|
||||||
|
throw new CannotDeleteOwnAccount();
|
||||||
|
}
|
||||||
await this.user.deleteUser(id);
|
await this.user.deleteUser(id);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
@@ -268,26 +275,22 @@ export class UserManagementResolver {
|
|||||||
@Args('input') input: ManageUserInput
|
@Args('input') input: ManageUserInput
|
||||||
): Promise<UserType> {
|
): Promise<UserType> {
|
||||||
const user = await this.db.user.findUnique({
|
const user = await this.db.user.findUnique({
|
||||||
select: { ...this.user.defaultUserSelect, password: true },
|
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UserNotFound();
|
throw new UserNotFound();
|
||||||
}
|
}
|
||||||
validators.assertValidEmail(input.email);
|
|
||||||
if (input.email !== user.email) {
|
input = omitBy(input, isNil);
|
||||||
const exists = await this.db.user.findFirst({
|
if (Object.keys(input).length === 0) {
|
||||||
where: { email: input.email },
|
return sessionUser(user);
|
||||||
});
|
|
||||||
if (exists) {
|
|
||||||
throw new Error('Email already exists');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionUser(
|
return sessionUser(
|
||||||
await this.user.updateUser(user.id, {
|
await this.user.updateUser(user.id, {
|
||||||
name: input.name,
|
|
||||||
email: input.email,
|
email: input.email,
|
||||||
|
name: input.name,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,9 +194,7 @@ export class UserService {
|
|||||||
|
|
||||||
async updateUser(
|
async updateUser(
|
||||||
id: string,
|
id: string,
|
||||||
data: Omit<Prisma.UserUpdateInput, 'password'> & {
|
data: Omit<Partial<Prisma.UserCreateInput>, 'id'>,
|
||||||
password?: string | null;
|
|
||||||
},
|
|
||||||
select: Prisma.UserSelect = this.defaultUserSelect
|
select: Prisma.UserSelect = this.defaultUserSelect
|
||||||
) {
|
) {
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
@@ -211,6 +209,23 @@ export class UserService {
|
|||||||
|
|
||||||
data.password = await this.crypto.encryptPassword(data.password);
|
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 });
|
const user = await this.prisma.user.update({ where: { id }, data, select });
|
||||||
|
|
||||||
this.emitter.emit('user.updated', user);
|
this.emitter.emit('user.updated', user);
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ export class UpdateUserInput implements Partial<User> {
|
|||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class ManageUserInput {
|
export class ManageUserInput {
|
||||||
|
@Field({ description: 'User email', nullable: true })
|
||||||
|
email?: string;
|
||||||
|
|
||||||
@Field({ description: 'User name', nullable: true })
|
@Field({ description: 'User name', nullable: true })
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@Field({ description: 'User email' })
|
|
||||||
email!: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '../../fundamentals/event/def' {
|
declare module '../../fundamentals/event/def' {
|
||||||
|
|||||||
@@ -498,4 +498,12 @@ export const USER_FRIENDLY_ERRORS = {
|
|||||||
type: 'internal_server_error',
|
type: 'internal_server_error',
|
||||||
message: 'Mailer service is not configured.',
|
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<string, UserFriendlyErrorOptions>;
|
} satisfies Record<string, UserFriendlyErrorOptions>;
|
||||||
|
|||||||
@@ -487,6 +487,18 @@ export class MailerServiceIsNotConfigured extends UserFriendlyError {
|
|||||||
super('internal_server_error', 'mailer_service_is_not_configured', message);
|
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 {
|
export enum ErrorNames {
|
||||||
INTERNAL_SERVER_ERROR,
|
INTERNAL_SERVER_ERROR,
|
||||||
TOO_MANY_REQUEST,
|
TOO_MANY_REQUEST,
|
||||||
@@ -551,7 +563,9 @@ export enum ErrorNames {
|
|||||||
COPILOT_QUOTA_EXCEEDED,
|
COPILOT_QUOTA_EXCEEDED,
|
||||||
RUNTIME_CONFIG_NOT_FOUND,
|
RUNTIME_CONFIG_NOT_FOUND,
|
||||||
INVALID_RUNTIME_CONFIG_TYPE,
|
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, {
|
registerEnumType(ErrorNames, {
|
||||||
name: 'ErrorNames'
|
name: 'ErrorNames'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { registerEnumType, ResolveField, Resolver } from '@nestjs/graphql';
|
import { registerEnumType, ResolveField, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { ServerConfigType } from '../../core/config';
|
import { ServerConfigType } from '../../core/config/types';
|
||||||
import { OAuthProviderName } from './config';
|
import { OAuthProviderName } from './config';
|
||||||
import { OAuthProviderFactory } from './register';
|
import { OAuthProviderFactory } from './register';
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ input CreateCopilotPromptInput {
|
|||||||
input CreateUserInput {
|
input CreateUserInput {
|
||||||
email: String!
|
email: String!
|
||||||
name: String
|
name: String
|
||||||
password: String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CredentialsRequirementType {
|
type CredentialsRequirementType {
|
||||||
@@ -196,11 +195,6 @@ type DocNotFoundDataType {
|
|||||||
workspaceId: String!
|
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
|
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 {
|
enum ErrorNames {
|
||||||
@@ -209,6 +203,8 @@ 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_OWN_ACCOUNT
|
||||||
CANT_CHANGE_WORKSPACE_OWNER
|
CANT_CHANGE_WORKSPACE_OWNER
|
||||||
CANT_UPDATE_LIFETIME_SUBSCRIPTION
|
CANT_UPDATE_LIFETIME_SUBSCRIPTION
|
||||||
COPILOT_ACTION_TAKEN
|
COPILOT_ACTION_TAKEN
|
||||||
@@ -398,7 +394,7 @@ input ListUserInput {
|
|||||||
|
|
||||||
input ManageUserInput {
|
input ManageUserInput {
|
||||||
"""User email"""
|
"""User email"""
|
||||||
email: String!
|
email: String
|
||||||
|
|
||||||
"""User name"""
|
"""User name"""
|
||||||
name: String
|
name: String
|
||||||
@@ -410,8 +406,6 @@ type MissingOauthQueryParameterDataType {
|
|||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
acceptInviteById(inviteId: String!, sendAcceptMail: Boolean, workspaceId: String!): Boolean!
|
acceptInviteById(inviteId: String!, sendAcceptMail: Boolean, workspaceId: String!): Boolean!
|
||||||
addAdminister(email: String!): Boolean!
|
|
||||||
addToEarlyAccess(email: String!, type: EarlyAccessType!): Int!
|
|
||||||
addWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
|
addWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
|
||||||
cancelSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription!
|
cancelSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription!
|
||||||
changeEmail(email: String!, token: String!): UserType!
|
changeEmail(email: String!, token: String!): UserType!
|
||||||
@@ -456,11 +450,9 @@ type Mutation {
|
|||||||
leaveWorkspace(sendLeaveMail: Boolean, workspaceId: String!, workspaceName: String!): Boolean!
|
leaveWorkspace(sendLeaveMail: Boolean, workspaceId: String!, workspaceName: String!): Boolean!
|
||||||
publishPage(mode: PublicPageMode = Page, pageId: String!, workspaceId: String!): WorkspacePage!
|
publishPage(mode: PublicPageMode = Page, pageId: String!, workspaceId: String!): WorkspacePage!
|
||||||
recoverDoc(guid: String!, timestamp: DateTime!, workspaceId: String!): DateTime!
|
recoverDoc(guid: String!, timestamp: DateTime!, workspaceId: String!): DateTime!
|
||||||
removeAdminister(email: String!): Boolean!
|
|
||||||
|
|
||||||
"""Remove user avatar"""
|
"""Remove user avatar"""
|
||||||
removeAvatar: RemoveAvatar!
|
removeAvatar: RemoveAvatar!
|
||||||
removeEarlyAccess(email: String!, type: EarlyAccessType!): Int!
|
|
||||||
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
|
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
|
||||||
resumeSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription!
|
resumeSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription!
|
||||||
revoke(userId: String!, workspaceId: String!): Boolean!
|
revoke(userId: String!, workspaceId: String!): Boolean!
|
||||||
@@ -489,6 +481,9 @@ type Mutation {
|
|||||||
"""Update a user"""
|
"""Update a user"""
|
||||||
updateUser(id: String!, input: ManageUserInput!): UserType!
|
updateUser(id: String!, input: ManageUserInput!): UserType!
|
||||||
|
|
||||||
|
"""update user enabled feature"""
|
||||||
|
updateUserFeatures(features: [FeatureType!]!, id: String!): [FeatureType!]!
|
||||||
|
|
||||||
"""Update workspace"""
|
"""Update workspace"""
|
||||||
updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType!
|
updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType!
|
||||||
|
|
||||||
@@ -532,7 +527,6 @@ type Query {
|
|||||||
|
|
||||||
"""Get current user"""
|
"""Get current user"""
|
||||||
currentUser: UserType
|
currentUser: UserType
|
||||||
earlyAccessUsers: [UserType!]!
|
|
||||||
error(name: ErrorNames!): ErrorDataUnion!
|
error(name: ErrorNames!): ErrorDataUnion!
|
||||||
|
|
||||||
"""send workspace invitation"""
|
"""send workspace invitation"""
|
||||||
|
|||||||
@@ -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<FeatureType[]>([]);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="flex flex-col h-full gap-1">
|
|
||||||
<div className=" flex justify-between items-center py-[10px] px-6">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="icon"
|
|
||||||
className="w-7 h-7"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={closePanel}
|
|
||||||
>
|
|
||||||
<XIcon size={20} />
|
|
||||||
</Button>
|
|
||||||
<span className="text-base font-medium">Create Account</span>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="icon"
|
|
||||||
className="w-7 h-7"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={disableSave}
|
|
||||||
>
|
|
||||||
<CheckIcon size={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="p-4 flex-grow overflow-y-auto space-y-[10px]">
|
|
||||||
<div className="flex flex-col rounded-md border py-4 gap-4">
|
|
||||||
<div className="px-5 space-y-3">
|
|
||||||
<Label className="text-sm font-medium">Name</Label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
className="py-2 px-3 text-base font-normal"
|
|
||||||
value={name}
|
|
||||||
onChange={e => setName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="px-5 space-y-3">
|
|
||||||
<Label className="text-sm font-medium">Email</Label>
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
className="py-2 px-3 ext-base font-normal"
|
|
||||||
value={email}
|
|
||||||
onChange={e => setEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>{' '}
|
|
||||||
<Separator />
|
|
||||||
<div className="px-5 space-y-3">
|
|
||||||
<Label className="text-sm font-medium">Password</Label>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
className="py-2 px-3 ext-base font-normal"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border rounded-md">
|
|
||||||
<Label className="flex items-center justify-between px-4 py-3">
|
|
||||||
<span>Enable AI Access</span>
|
|
||||||
<Switch
|
|
||||||
checked={features.includes(FeatureType.AIEarlyAccess)}
|
|
||||||
onCheckedChange={onEarlyAccessChange}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
<Separator />
|
|
||||||
<Label className="flex items-center justify-between px-4 py-3">
|
|
||||||
<span>Admin</span>
|
|
||||||
<Switch
|
|
||||||
checked={features.includes(FeatureType.Admin)}
|
|
||||||
onCheckedChange={onAdminChange}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
} from '@affine/admin/components/ui/avatar';
|
} from '@affine/admin/components/ui/avatar';
|
||||||
|
import type { UserType } from '@affine/graphql';
|
||||||
import { FeatureType } from '@affine/graphql';
|
import { FeatureType } from '@affine/graphql';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import type { User } from '../schema';
|
|
||||||
import { DataTableRowActions } from './data-table-row-actions';
|
import { DataTableRowActions } from './data-table-row-actions';
|
||||||
|
|
||||||
const StatusItem = ({
|
const StatusItem = ({
|
||||||
@@ -51,7 +51,7 @@ const StatusItem = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const columns: ColumnDef<User>[] = [
|
export const columns: ColumnDef<UserType>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'info',
|
accessorKey: 'info',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
@@ -88,13 +88,13 @@ export const columns: ColumnDef<User>[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'property',
|
accessorKey: 'property',
|
||||||
cell: ({ row }) => (
|
cell: ({ row: { original: user } }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex flex-col gap-2 text-xs max-md:hidden">
|
<div className="flex flex-col gap-2 text-xs max-md:hidden">
|
||||||
<div className="flex justify-end opacity-25">{row.original.id}</div>
|
<div className="flex justify-end opacity-25">{user.id}</div>
|
||||||
<div className="flex gap-3 items-center justify-end">
|
<div className="flex gap-3 items-center justify-end">
|
||||||
<StatusItem
|
<StatusItem
|
||||||
condition={row.original.hasPassword}
|
condition={user.hasPassword}
|
||||||
IconTrue={<LockIcon size={10} />}
|
IconTrue={<LockIcon size={10} />}
|
||||||
IconFalse={<UnlockIcon size={10} />}
|
IconFalse={<UnlockIcon size={10} />}
|
||||||
textTrue="Password Set"
|
textTrue="Password Set"
|
||||||
@@ -102,7 +102,7 @@ export const columns: ColumnDef<User>[] = [
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
condition={row.original.emailVerified}
|
condition={user.emailVerified}
|
||||||
IconTrue={<MailIcon size={10} />}
|
IconTrue={<MailIcon size={10} />}
|
||||||
IconFalse={<MailWarningIcon size={10} />}
|
IconFalse={<MailWarningIcon size={10} />}
|
||||||
textTrue="Email Verified"
|
textTrue="Email Verified"
|
||||||
@@ -110,7 +110,7 @@ export const columns: ColumnDef<User>[] = [
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataTableRowActions row={row} />
|
<DataTableRowActions user={user} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@affine/admin/components/ui/dropdown-menu';
|
} from '@affine/admin/components/ui/dropdown-menu';
|
||||||
import type { Row } from '@tanstack/react-table';
|
|
||||||
import {
|
import {
|
||||||
LockIcon,
|
LockIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
@@ -17,29 +16,26 @@ import { useCallback, useState } from 'react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { useRightPanel } from '../../layout';
|
import { useRightPanel } from '../../layout';
|
||||||
import { userSchema } from '../schema';
|
import type { UserType } from '../schema';
|
||||||
import { DeleteAccountDialog } from './delete-account';
|
import { DeleteAccountDialog } from './delete-account';
|
||||||
import { DiscardChanges } from './discard-changes';
|
import { DiscardChanges } from './discard-changes';
|
||||||
import { EditPanel } from './edit-panel';
|
|
||||||
import { ResetPasswordDialog } from './reset-password';
|
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<TData> {
|
interface DataTableRowActionsProps {
|
||||||
row: Row<TData>;
|
user: UserType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableRowActions<TData>({
|
export function DataTableRowActions({ user }: DataTableRowActionsProps) {
|
||||||
row,
|
|
||||||
}: DataTableRowActionsProps<TData>) {
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false);
|
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false);
|
||||||
const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
|
const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
|
||||||
const user = userSchema.parse(row.original);
|
|
||||||
const { setRightPanelContent, openPanel, isOpen, closePanel } =
|
const { setRightPanelContent, openPanel, isOpen, closePanel } =
|
||||||
useRightPanel();
|
useRightPanel();
|
||||||
|
|
||||||
const { deleteUser, resetPasswordLink, onResetPassword } =
|
const deleteUser = useDeleteUser();
|
||||||
useUserManagement();
|
const { resetPasswordLink, onResetPassword } = useResetUserPassword();
|
||||||
|
|
||||||
const openResetPasswordDialog = useCallback(() => {
|
const openResetPasswordDialog = useCallback(() => {
|
||||||
onResetPassword(user.id, () => setResetPasswordDialogOpen(true));
|
onResetPassword(user.id, () => setResetPasswordDialogOpen(true));
|
||||||
@@ -82,8 +78,9 @@ export function DataTableRowActions<TData>({
|
|||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
setRightPanelContent(
|
setRightPanelContent(
|
||||||
<EditPanel
|
<UpdateUserForm
|
||||||
user={user}
|
user={user}
|
||||||
|
onComplete={closePanel}
|
||||||
onResetPassword={openResetPasswordDialog}
|
onResetPassword={openResetPasswordDialog}
|
||||||
onDeleteAccount={openDeleteDialog}
|
onDeleteAccount={openDeleteDialog}
|
||||||
/>
|
/>
|
||||||
@@ -96,6 +93,7 @@ export function DataTableRowActions<TData>({
|
|||||||
openPanel();
|
openPanel();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
closePanel,
|
||||||
discardDialogOpen,
|
discardDialogOpen,
|
||||||
handleDiscardChangesCancel,
|
handleDiscardChangesCancel,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import type { SetStateAction } from 'react';
|
|||||||
import { startTransition, useCallback, useEffect, useState } from 'react';
|
import { startTransition, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRightPanel } from '../../layout';
|
import { useRightPanel } from '../../layout';
|
||||||
import { CreateUserPanel } from './ceate-user-panel';
|
|
||||||
import { DiscardChanges } from './discard-changes';
|
import { DiscardChanges } from './discard-changes';
|
||||||
|
import { CreateUserForm } from './user-form';
|
||||||
|
|
||||||
interface DataTableToolbarProps<TData> {
|
interface DataTableToolbarProps<TData> {
|
||||||
data: TData[];
|
data: TData[];
|
||||||
@@ -38,17 +38,18 @@ export function DataTableToolbar<TData>({
|
|||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const debouncedValue = useDebouncedValue(value, 500);
|
const debouncedValue = useDebouncedValue(value, 500);
|
||||||
const { setRightPanelContent, openPanel, isOpen } = useRightPanel();
|
const { setRightPanelContent, openPanel, closePanel, isOpen } =
|
||||||
|
useRightPanel();
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
setRightPanelContent(<CreateUserPanel />);
|
setRightPanelContent(<CreateUserForm onComplete={closePanel} />);
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}
|
}
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
openPanel();
|
openPanel();
|
||||||
}
|
}
|
||||||
}, [setRightPanelContent, dialogOpen, isOpen, openPanel]);
|
}, [setRightPanelContent, closePanel, dialogOpen, isOpen, openPanel]);
|
||||||
|
|
||||||
const result = useQuery({
|
const result = useQuery({
|
||||||
query: getUserByEmailQuery,
|
query: getUserByEmailQuery,
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<div className="flex flex-col h-full gap-1">
|
|
||||||
<div className=" flex justify-between items-center py-[10px] px-6 ">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="icon"
|
|
||||||
className="w-7 h-7"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={closePanel}
|
|
||||||
>
|
|
||||||
<XIcon size={20} />
|
|
||||||
</Button>
|
|
||||||
<span className="text-base font-medium">Edit Account</span>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="icon"
|
|
||||||
className="w-7 h-7"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={onConfirm}
|
|
||||||
disabled={disableSave}
|
|
||||||
>
|
|
||||||
<CheckIcon size={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="p-4 flex-grow overflow-y-auto space-y-[10px]">
|
|
||||||
<div className="flex flex-col rounded-md border py-4 gap-4">
|
|
||||||
<div className="px-5 space-y-3">
|
|
||||||
<Label className="text-sm font-medium">Name</Label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
className="py-2 px-3 text-base font-normal"
|
|
||||||
value={name}
|
|
||||||
onChange={e => setName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="px-5 space-y-3">
|
|
||||||
<Label className="text-sm font-medium">Email</Label>
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
className="py-2 px-3 ext-base font-normal"
|
|
||||||
value={email}
|
|
||||||
onChange={e => setEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="w-full flex items-center justify-between text-sm font-medium px-4 py-3"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onResetPassword}
|
|
||||||
>
|
|
||||||
<span>Reset Password</span>
|
|
||||||
<ChevronRightIcon size={16} />
|
|
||||||
</Button>
|
|
||||||
<div className="border rounded-md">
|
|
||||||
<Label className="flex items-center justify-between px-4 py-3">
|
|
||||||
<span>Enable AI Access</span>
|
|
||||||
<Switch
|
|
||||||
checked={features.includes(FeatureType.AIEarlyAccess)}
|
|
||||||
onCheckedChange={onEarlyAccessChange}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
<Separator />
|
|
||||||
<Label className="flex items-center justify-between px-4 py-3">
|
|
||||||
<span>Admin</span>
|
|
||||||
<Switch
|
|
||||||
checked={features.includes(FeatureType.Admin)}
|
|
||||||
onCheckedChange={onAdminChange}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="w-full text-red-500 px-4 py-3 rounded-md flex items-center justify-between text-sm font-medium hover:text-red-500"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onDeleteAccount}
|
|
||||||
>
|
|
||||||
<span>Delete Account</span>
|
|
||||||
<ChevronRightIcon size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,161 +4,103 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
} from '@affine/core/hooks/use-mutation';
|
} from '@affine/core/hooks/use-mutation';
|
||||||
import {
|
import {
|
||||||
addToAdminMutation,
|
|
||||||
addToEarlyAccessMutation,
|
|
||||||
createChangePasswordUrlMutation,
|
createChangePasswordUrlMutation,
|
||||||
createUserMutation,
|
createUserMutation,
|
||||||
deleteUserMutation,
|
deleteUserMutation,
|
||||||
EarlyAccessType,
|
|
||||||
FeatureType,
|
|
||||||
listUsersQuery,
|
listUsersQuery,
|
||||||
removeAdminMutation,
|
updateAccountFeaturesMutation,
|
||||||
removeEarlyAccessMutation,
|
|
||||||
updateAccountMutation,
|
updateAccountMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import type { UserInput } from '../schema';
|
||||||
|
|
||||||
export const useCreateUser = () => {
|
export const useCreateUser = () => {
|
||||||
const { trigger: createUser } = useMutation({
|
const {
|
||||||
|
trigger: createAccount,
|
||||||
|
isMutating: creating,
|
||||||
|
error,
|
||||||
|
} = useMutation({
|
||||||
mutation: createUserMutation,
|
mutation: createUserMutation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { trigger: addToEarlyAccess } = useMutation({
|
const { trigger: updateAccountFeatures } = useMutation({
|
||||||
mutation: addToEarlyAccessMutation,
|
mutation: updateAccountFeaturesMutation,
|
||||||
});
|
|
||||||
|
|
||||||
const { trigger: addToAdmin } = useMutation({
|
|
||||||
mutation: addToAdminMutation,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const revalidate = useMutateQueryResource();
|
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(
|
const create = useAsyncCallback(
|
||||||
async ({
|
async ({ name, email, features }: UserInput) => {
|
||||||
name,
|
try {
|
||||||
email,
|
const account = await createAccount({
|
||||||
password,
|
input: {
|
||||||
features,
|
name,
|
||||||
callback,
|
email,
|
||||||
}: {
|
},
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 = () => {
|
export const useUpdateUser = () => {
|
||||||
const { trigger: updateAccount } = useMutation({
|
const {
|
||||||
|
trigger: updateAccount,
|
||||||
|
isMutating: updating,
|
||||||
|
error,
|
||||||
|
} = useMutation({
|
||||||
mutation: updateAccountMutation,
|
mutation: updateAccountMutation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { trigger: addToEarlyAccess } = useMutation({
|
const { trigger: updateAccountFeatures } = useMutation({
|
||||||
mutation: addToEarlyAccessMutation,
|
mutation: updateAccountFeaturesMutation,
|
||||||
});
|
|
||||||
|
|
||||||
const { trigger: removeEarlyAccess } = useMutation({
|
|
||||||
mutation: removeEarlyAccessMutation,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { trigger: addToAdmin } = useMutation({
|
|
||||||
mutation: addToAdminMutation,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { trigger: removeAdmin } = useMutation({
|
|
||||||
mutation: removeAdminMutation,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const revalidate = useMutateQueryResource();
|
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(
|
const update = useAsyncCallback(
|
||||||
async ({ userId, name, email, features, callback }: UpdateUserProps) => {
|
async ({
|
||||||
updateAccount({
|
userId,
|
||||||
id: userId,
|
name,
|
||||||
input: {
|
email,
|
||||||
name,
|
features,
|
||||||
email,
|
}: UserInput & { userId: string }) => {
|
||||||
},
|
try {
|
||||||
})
|
await updateAccount({
|
||||||
.then(async () => {
|
id: userId,
|
||||||
await updateFeatures({ email, features });
|
input: {
|
||||||
await revalidate(listUsersQuery);
|
name,
|
||||||
toast('Account updated successfully');
|
email,
|
||||||
callback?.();
|
},
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
toast.error('Failed to update account: ' + e.message);
|
|
||||||
});
|
});
|
||||||
|
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 = () => {
|
export const useResetUserPassword = () => {
|
||||||
@@ -217,20 +159,3 @@ export const useDeleteUser = () => {
|
|||||||
|
|
||||||
return deleteById;
|
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]);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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<UserInput>;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: (user: UserInput) => void;
|
||||||
|
onValidate: (user: Partial<UserInput>) => boolean;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function UserForm({
|
||||||
|
title,
|
||||||
|
defaultValue,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
onValidate,
|
||||||
|
actions,
|
||||||
|
}: UserFormProps) {
|
||||||
|
const serverConfig = useServerConfig();
|
||||||
|
|
||||||
|
const [changes, setChanges] = useState<Partial<UserInput>>({
|
||||||
|
features: defaultValue?.features ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const setField = useCallback(
|
||||||
|
<K extends keyof UserInput>(
|
||||||
|
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 (
|
||||||
|
<div className="flex flex-col h-full gap-1">
|
||||||
|
<div className=" flex justify-between items-center py-[10px] px-6">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
className="w-7 h-7"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<XIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
<span className="text-base font-medium">{title}</span>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="icon"
|
||||||
|
className="w-7 h-7"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={!canSave}
|
||||||
|
>
|
||||||
|
<CheckIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="p-4 flex-grow overflow-y-auto space-y-[10px]">
|
||||||
|
<div className="flex flex-col rounded-md border py-4 gap-4">
|
||||||
|
<InputItem
|
||||||
|
label="Name"
|
||||||
|
field="name"
|
||||||
|
value={changes.name ?? defaultValue?.name}
|
||||||
|
onChange={setField}
|
||||||
|
/>
|
||||||
|
<Separator />
|
||||||
|
<InputItem
|
||||||
|
label="Email"
|
||||||
|
field="email"
|
||||||
|
value={changes.email ?? defaultValue?.email}
|
||||||
|
onChange={setField}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md">
|
||||||
|
{serverConfig.availableUserFeatures.map((feature, i) => (
|
||||||
|
<div key={feature}>
|
||||||
|
<ToggleItem
|
||||||
|
name={feature}
|
||||||
|
checked={(
|
||||||
|
changes.features ??
|
||||||
|
defaultValue?.features ??
|
||||||
|
[]
|
||||||
|
).includes(feature)}
|
||||||
|
onChange={onFeatureChanged}
|
||||||
|
/>
|
||||||
|
{i < serverConfig.availableUserFeatures.length - 1 && (
|
||||||
|
<Separator />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Label className="flex items-center justify-between px-4 py-3">
|
||||||
|
<span>{name}</span>
|
||||||
|
<Switch checked={checked} onCheckedChange={onToggle} />
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
onChange(field, e.target.value);
|
||||||
|
},
|
||||||
|
[field, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-5 space-y-3">
|
||||||
|
<Label className="text-sm font-medium">{label}</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className="py-2 px-3 text-base font-normal"
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={onValueChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateCreateUser = (user: Partial<UserInput>) => {
|
||||||
|
return !!user.name && !!user.email && !!user.features;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateUpdateUser = (user: Partial<UserInput>) => {
|
||||||
|
return !!user.name || !!user.email;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CreateUserForm({ onComplete }: { onComplete: () => void }) {
|
||||||
|
const { create, creating } = useCreateUser();
|
||||||
|
useEffect(() => {
|
||||||
|
if (creating) {
|
||||||
|
return () => {
|
||||||
|
onComplete();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}, [creating, onComplete]);
|
||||||
|
return (
|
||||||
|
<UserForm
|
||||||
|
title="Create User"
|
||||||
|
onClose={onComplete}
|
||||||
|
onConfirm={create}
|
||||||
|
onValidate={validateCreateUser}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<UserForm
|
||||||
|
title="Update User"
|
||||||
|
defaultValue={user}
|
||||||
|
onClose={onComplete}
|
||||||
|
onConfirm={onUpdateUser}
|
||||||
|
onValidate={validateUpdateUser}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="w-full flex items-center justify-between text-sm font-medium px-4 py-3"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onResetPassword}
|
||||||
|
>
|
||||||
|
<span>Reset Password</span>
|
||||||
|
<ChevronRightIcon size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full text-red-500 px-4 py-3 rounded-md flex items-center justify-between text-sm font-medium hover:text-red-500"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onDeleteAccount}
|
||||||
|
>
|
||||||
|
<span>Delete Account</span>
|
||||||
|
<ChevronRightIcon size={16} />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ export function AccountPage() {
|
|||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={users}
|
data={users}
|
||||||
|
// @ts-expect-error do not complains
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPaginationChange={setPagination}
|
onPaginationChange={setPagination}
|
||||||
|
|||||||
@@ -1,34 +1,8 @@
|
|||||||
import { FeatureType } from '@affine/graphql';
|
import type { FeatureType, ListUsersQuery } from '@affine/graphql';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const featureTypeValues = Object.values(FeatureType) as [
|
export type UserType = ListUsersQuery['users'][0];
|
||||||
FeatureType,
|
export type UserInput = {
|
||||||
...FeatureType[],
|
name: string;
|
||||||
];
|
email: string;
|
||||||
const featureTypeEnum = z.enum(featureTypeValues);
|
features: FeatureType[];
|
||||||
|
};
|
||||||
export const userSchema = z.object({
|
|
||||||
__typename: z.literal('UserType').optional(),
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
email: z.string(),
|
|
||||||
features: z.array(featureTypeEnum),
|
|
||||||
hasPassword: z.boolean().nullable(),
|
|
||||||
emailVerified: z.boolean(),
|
|
||||||
avatarUrl: z.string().nullable(),
|
|
||||||
quota: z
|
|
||||||
.object({
|
|
||||||
__typename: z.literal('UserQuota').optional(),
|
|
||||||
humanReadable: z.object({
|
|
||||||
__typename: z.literal('UserQuotaHumanReadable').optional(),
|
|
||||||
blobLimit: z.string(),
|
|
||||||
historyPeriod: z.string(),
|
|
||||||
memberLimit: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
storageQuota: z.string(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type User = z.infer<typeof userSchema>;
|
|
||||||
|
|||||||
@@ -2,31 +2,21 @@ import { Button } from '@affine/admin/components/ui/button';
|
|||||||
import { Input } from '@affine/admin/components/ui/input';
|
import { Input } from '@affine/admin/components/ui/input';
|
||||||
import { Label } from '@affine/admin/components/ui/label';
|
import { Label } from '@affine/admin/components/ui/label';
|
||||||
import { useMutateQueryResource } from '@affine/core/hooks/use-mutation';
|
import { useMutateQueryResource } from '@affine/core/hooks/use-mutation';
|
||||||
import { useQuery } from '@affine/core/hooks/use-query';
|
|
||||||
import {
|
import {
|
||||||
FeatureType,
|
FeatureType,
|
||||||
getCurrentUserFeaturesQuery,
|
getCurrentUserFeaturesQuery,
|
||||||
getUserFeaturesQuery,
|
getUserFeaturesQuery,
|
||||||
serverConfigQuery,
|
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { useCurrentUser, useServerConfig } from '../common';
|
||||||
import logo from './logo.svg';
|
import logo from './logo.svg';
|
||||||
|
|
||||||
export function Auth() {
|
export function Auth() {
|
||||||
const {
|
const currentUser = useCurrentUser();
|
||||||
data: { currentUser },
|
const serverConfig = useServerConfig();
|
||||||
} = useQuery({
|
|
||||||
query: getCurrentUserFeaturesQuery,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { serverConfig },
|
|
||||||
} = useQuery({
|
|
||||||
query: serverConfigQuery,
|
|
||||||
});
|
|
||||||
const revalidate = useMutateQueryResource();
|
const revalidate = useMutateQueryResource();
|
||||||
const emailRef = useRef<HTMLInputElement>(null);
|
const emailRef = useRef<HTMLInputElement>(null);
|
||||||
const passwordRef = useRef<HTMLInputElement>(null);
|
const passwordRef = useRef<HTMLInputElement>(null);
|
||||||
|
|||||||
21
packages/frontend/admin/src/modules/common.ts
Normal file
21
packages/frontend/admin/src/modules/common.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '@affine/admin/components/ui/card';
|
} from '@affine/admin/components/ui/card';
|
||||||
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
||||||
import { Separator } from '@affine/admin/components/ui/separator';
|
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 { getServerServiceConfigsQuery } from '@affine/graphql';
|
||||||
|
|
||||||
import { Layout } from '../layout';
|
import { Layout } from '../layout';
|
||||||
@@ -171,7 +171,7 @@ const MailerCard = ({ mailerConfig }: { mailerConfig?: MailerConfig }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ServerServiceConfig() {
|
export function ServerServiceConfig() {
|
||||||
const { data } = useQuery({
|
const { data } = useQueryImmutable({
|
||||||
query: getServerServiceConfigsQuery,
|
query: getServerServiceConfigsQuery,
|
||||||
});
|
});
|
||||||
const server = data.serverServiceConfigs.find(
|
const server = data.serverServiceConfigs.find(
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ import { Separator } from '@affine/admin/components/ui/separator';
|
|||||||
import { TooltipProvider } from '@affine/admin/components/ui/tooltip';
|
import { TooltipProvider } from '@affine/admin/components/ui/tooltip';
|
||||||
import { cn } from '@affine/admin/utils';
|
import { cn } from '@affine/admin/utils';
|
||||||
import { useQuery } from '@affine/core/hooks/use-query';
|
import { useQuery } from '@affine/core/hooks/use-query';
|
||||||
import {
|
import { FeatureType, getCurrentUserFeaturesQuery } from '@affine/graphql';
|
||||||
FeatureType,
|
|
||||||
getCurrentUserFeaturesQuery,
|
|
||||||
serverConfigQuery,
|
|
||||||
} from '@affine/graphql';
|
|
||||||
import { AlignJustifyIcon } from 'lucide-react';
|
import { AlignJustifyIcon } from 'lucide-react';
|
||||||
import type { ReactNode, RefObject } from 'react';
|
import type { ReactNode, RefObject } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +32,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '../components/ui/sheet';
|
} from '../components/ui/sheet';
|
||||||
import { Logo } from './accounts/components/logo';
|
import { Logo } from './accounts/components/logo';
|
||||||
|
import { useServerConfig } from './common';
|
||||||
import { NavContext } from './nav/context';
|
import { NavContext } from './nav/context';
|
||||||
import { Nav } from './nav/nav';
|
import { Nav } from './nav/nav';
|
||||||
|
|
||||||
@@ -85,6 +82,13 @@ export function useMediaQuery(query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Layout({ content }: LayoutProps) {
|
export function Layout({ content }: LayoutProps) {
|
||||||
|
const serverConfig = useServerConfig();
|
||||||
|
const {
|
||||||
|
data: { currentUser },
|
||||||
|
} = useQuery({
|
||||||
|
query: getCurrentUserFeaturesQuery,
|
||||||
|
});
|
||||||
|
|
||||||
const [rightPanelContent, setRightPanelContent] = useState<ReactNode>(null);
|
const [rightPanelContent, setRightPanelContent] = useState<ReactNode>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const rightPanelRef = useRef<ImperativePanelHandle>(null);
|
const rightPanelRef = useRef<ImperativePanelHandle>(null);
|
||||||
@@ -122,16 +126,6 @@ export function Layout({ content }: LayoutProps) {
|
|||||||
[closePanel, openPanel]
|
[closePanel, openPanel]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
|
||||||
data: { serverConfig },
|
|
||||||
} = useQuery({
|
|
||||||
query: serverConfigQuery,
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
data: { currentUser },
|
|
||||||
} = useQuery({
|
|
||||||
query: getCurrentUserFeaturesQuery,
|
|
||||||
});
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -12,29 +12,17 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@affine/admin/components/ui/dropdown-menu';
|
} from '@affine/admin/components/ui/dropdown-menu';
|
||||||
import { useQuery } from '@affine/core/hooks/use-query';
|
import { FeatureType } from '@affine/graphql';
|
||||||
import {
|
|
||||||
FeatureType,
|
|
||||||
getCurrentUserFeaturesQuery,
|
|
||||||
serverConfigQuery,
|
|
||||||
} from '@affine/graphql';
|
|
||||||
import { CircleUser, MoreVertical } from 'lucide-react';
|
import { CircleUser, MoreVertical } from 'lucide-react';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export function UserDropdown() {
|
import { useCurrentUser, useServerConfig } from '../common';
|
||||||
const {
|
|
||||||
data: { currentUser },
|
|
||||||
} = useQuery({
|
|
||||||
query: getCurrentUserFeaturesQuery,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
export function UserDropdown() {
|
||||||
data: { serverConfig },
|
const currentUser = useCurrentUser();
|
||||||
} = useQuery({
|
const serverConfig = useServerConfig();
|
||||||
query: serverConfigQuery,
|
|
||||||
});
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
} from '@affine/admin/components/ui/carousel';
|
} from '@affine/admin/components/ui/carousel';
|
||||||
import { validateEmailAndPassword } from '@affine/admin/utils';
|
import { validateEmailAndPassword } from '@affine/admin/utils';
|
||||||
import { useMutateQueryResource } from '@affine/core/hooks/use-mutation';
|
import { useMutateQueryResource } from '@affine/core/hooks/use-mutation';
|
||||||
import { useQuery } from '@affine/core/hooks/use-query';
|
|
||||||
import { serverConfigQuery } from '@affine/graphql';
|
import { serverConfigQuery } from '@affine/graphql';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { useServerConfig } from '../common';
|
||||||
import { CreateAdmin } from './create-admin';
|
import { CreateAdmin } from './create-admin';
|
||||||
|
|
||||||
export enum CarouselSteps {
|
export enum CarouselSteps {
|
||||||
@@ -72,10 +72,8 @@ export const Form = () => {
|
|||||||
const [invalidEmail, setInvalidEmail] = useState(false);
|
const [invalidEmail, setInvalidEmail] = useState(false);
|
||||||
const [invalidPassword, setInvalidPassword] = useState(false);
|
const [invalidPassword, setInvalidPassword] = useState(false);
|
||||||
|
|
||||||
const { data } = useQuery({
|
const serverConfig = useServerConfig();
|
||||||
query: serverConfigQuery,
|
const passwordLimits = serverConfig.credentialsRequirement.password;
|
||||||
});
|
|
||||||
const passwordLimits = data.serverConfig.credentialsRequirement.password;
|
|
||||||
|
|
||||||
const isCreateAdminStep = current - 1 === CarouselSteps.CreateAdmin;
|
const isCreateAdminStep = current - 1 === CarouselSteps.CreateAdmin;
|
||||||
|
|
||||||
@@ -95,7 +93,7 @@ export const Form = () => {
|
|||||||
api.on('select', () => {
|
api.on('select', () => {
|
||||||
setCurrent(api.selectedScrollSnap() + 1);
|
setCurrent(api.selectedScrollSnap() + 1);
|
||||||
});
|
});
|
||||||
}, [api, data.serverConfig.initialized, navigate]);
|
}, [api, serverConfig.initialized, navigate]);
|
||||||
|
|
||||||
const createAdmin = useCallback(async () => {
|
const createAdmin = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -170,14 +168,14 @@ export const Form = () => {
|
|||||||
|
|
||||||
const onPrevious = useCallback(() => {
|
const onPrevious = useCallback(() => {
|
||||||
if (current === count) {
|
if (current === count) {
|
||||||
if (data.serverConfig.initialized === true) {
|
if (serverConfig.initialized === true) {
|
||||||
return navigate('/admin', { replace: true });
|
return navigate('/admin', { replace: true });
|
||||||
}
|
}
|
||||||
toast.error('Goto Admin Panel failed, please try again.');
|
toast.error('Goto Admin Panel failed, please try again.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
api?.scrollPrev();
|
api?.scrollPrev();
|
||||||
}, [api, count, current, data.serverConfig.initialized, navigate]);
|
}, [api, count, current, serverConfig.initialized, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-between h-full w-full lg:pl-36 max-lg:items-center ">
|
<div className="flex flex-col justify-between h-full w-full lg:pl-36 max-lg:items-center ">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function useMutation<Mutation extends GraphQLQuery, K extends Key = Key>(
|
|||||||
config?: Omit<
|
config?: Omit<
|
||||||
SWRMutationConfiguration<
|
SWRMutationConfiguration<
|
||||||
QueryResponse<Mutation>,
|
QueryResponse<Mutation>,
|
||||||
GraphQLError | GraphQLError[],
|
GraphQLError,
|
||||||
K,
|
K,
|
||||||
QueryVariables<Mutation>
|
QueryVariables<Mutation>
|
||||||
>,
|
>,
|
||||||
@@ -43,7 +43,7 @@ export function useMutation<Mutation extends GraphQLQuery, K extends Key = Key>(
|
|||||||
>
|
>
|
||||||
): SWRMutationResponse<
|
): SWRMutationResponse<
|
||||||
QueryResponse<Mutation>,
|
QueryResponse<Mutation>,
|
||||||
GraphQLError | GraphQLError[],
|
GraphQLError,
|
||||||
K,
|
K,
|
||||||
QueryVariables<Mutation>
|
QueryVariables<Mutation>
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -32,16 +32,12 @@ import useSWRInfinite from 'swr/infinite';
|
|||||||
type useQueryFn = <Query extends GraphQLQuery>(
|
type useQueryFn = <Query extends GraphQLQuery>(
|
||||||
options?: QueryOptions<Query>,
|
options?: QueryOptions<Query>,
|
||||||
config?: Omit<
|
config?: Omit<
|
||||||
SWRConfiguration<
|
SWRConfiguration<QueryResponse<Query>, GraphQLError, typeof fetcher<Query>>,
|
||||||
QueryResponse<Query>,
|
|
||||||
GraphQLError | GraphQLError[],
|
|
||||||
typeof fetcher<Query>
|
|
||||||
>,
|
|
||||||
'fetcher'
|
'fetcher'
|
||||||
>
|
>
|
||||||
) => SWRResponse<
|
) => SWRResponse<
|
||||||
QueryResponse<Query>,
|
QueryResponse<Query>,
|
||||||
GraphQLError | GraphQLError[],
|
GraphQLError,
|
||||||
{
|
{
|
||||||
suspense: true;
|
suspense: true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
mutation addToAdmin($email: String!) {
|
|
||||||
addAdminister(email: $email)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
mutation addToEarlyAccess($email: String!, $type: EarlyAccessType!) {
|
|
||||||
addToEarlyAccess(email: $email, type: $type)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
query earlyAccessUsers {
|
|
||||||
earlyAccessUsers {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
email
|
|
||||||
avatarUrl
|
|
||||||
emailVerified
|
|
||||||
subscription {
|
|
||||||
plan
|
|
||||||
recurring
|
|
||||||
status
|
|
||||||
start
|
|
||||||
end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
mutation removeEarlyAccess($email: String!, $type: EarlyAccessType!) {
|
|
||||||
removeEarlyAccess(email: $email, type: $type)
|
|
||||||
}
|
|
||||||
@@ -18,15 +18,27 @@ fragment CredentialsRequirement on CredentialsRequirementType {
|
|||||||
...PasswordLimits
|
...PasswordLimits
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
export const addToAdminMutation = {
|
export const adminServerConfigQuery = {
|
||||||
id: 'addToAdminMutation' as const,
|
id: 'adminServerConfigQuery' as const,
|
||||||
operationName: 'addToAdmin',
|
operationName: 'adminServerConfig',
|
||||||
definitionName: 'addAdminister',
|
definitionName: 'serverConfig',
|
||||||
containsFile: false,
|
containsFile: false,
|
||||||
query: `
|
query: `
|
||||||
mutation addToAdmin($email: String!) {
|
query adminServerConfig {
|
||||||
addAdminister(email: $email)
|
serverConfig {
|
||||||
}`,
|
version
|
||||||
|
baseUrl
|
||||||
|
name
|
||||||
|
features
|
||||||
|
type
|
||||||
|
initialized
|
||||||
|
credentialsRequirement {
|
||||||
|
...CredentialsRequirement
|
||||||
|
}
|
||||||
|
availableUserFeatures
|
||||||
|
}
|
||||||
|
}${passwordLimitsFragment}
|
||||||
|
${credentialsRequirementFragment}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteBlobMutation = {
|
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 = {
|
export const forkCopilotSessionMutation = {
|
||||||
id: 'forkCopilotSessionMutation' as const,
|
id: 'forkCopilotSessionMutation' as const,
|
||||||
operationName: 'forkCopilotSession',
|
operationName: 'forkCopilotSession',
|
||||||
@@ -803,15 +769,6 @@ query listUsers($filter: ListUserInput!) {
|
|||||||
hasPassword
|
hasPassword
|
||||||
emailVerified
|
emailVerified
|
||||||
avatarUrl
|
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 = {
|
export const removeAvatarMutation = {
|
||||||
id: 'removeAvatarMutation' as const,
|
id: 'removeAvatarMutation' as const,
|
||||||
operationName: 'removeAvatar',
|
operationName: 'removeAvatar',
|
||||||
@@ -1024,7 +970,6 @@ query serverConfig {
|
|||||||
name
|
name
|
||||||
features
|
features
|
||||||
type
|
type
|
||||||
initialized
|
|
||||||
credentialsRequirement {
|
credentialsRequirement {
|
||||||
...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 = {
|
export const updateAccountMutation = {
|
||||||
id: 'updateAccountMutation' as const,
|
id: 'updateAccountMutation' as const,
|
||||||
operationName: 'updateAccount',
|
operationName: 'updateAccount',
|
||||||
|
|||||||
@@ -7,14 +7,5 @@ query listUsers($filter: ListUserInput!) {
|
|||||||
hasPassword
|
hasPassword
|
||||||
emailVerified
|
emailVerified
|
||||||
avatarUrl
|
avatarUrl
|
||||||
quota {
|
|
||||||
humanReadable {
|
|
||||||
blobLimit
|
|
||||||
historyPeriod
|
|
||||||
memberLimit
|
|
||||||
name
|
|
||||||
storageQuota
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
mutation removeAdmin($email: String!) {
|
|
||||||
removeAdminister(email: $email)
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ query serverConfig {
|
|||||||
name
|
name
|
||||||
features
|
features
|
||||||
type
|
type
|
||||||
initialized
|
|
||||||
credentialsRequirement {
|
credentialsRequirement {
|
||||||
...CredentialsRequirement
|
...CredentialsRequirement
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mutation updateAccountFeatures($userId: String!, $features: [FeatureType!]!) {
|
||||||
|
updateUserFeatures(id: $userId, features: $features)
|
||||||
|
}
|
||||||
@@ -200,7 +200,6 @@ export interface CreateCopilotPromptInput {
|
|||||||
export interface CreateUserInput {
|
export interface CreateUserInput {
|
||||||
email: Scalars['String']['input'];
|
email: Scalars['String']['input'];
|
||||||
name: InputMaybe<Scalars['String']['input']>;
|
name: InputMaybe<Scalars['String']['input']>;
|
||||||
password: InputMaybe<Scalars['String']['input']>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CredentialsRequirementType {
|
export interface CredentialsRequirementType {
|
||||||
@@ -245,11 +244,6 @@ export interface DocNotFoundDataType {
|
|||||||
workspaceId: Scalars['String']['output'];
|
workspaceId: Scalars['String']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EarlyAccessType {
|
|
||||||
AI = 'AI',
|
|
||||||
App = 'App',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ErrorDataUnion =
|
export type ErrorDataUnion =
|
||||||
| BlobNotFoundDataType
|
| BlobNotFoundDataType
|
||||||
| CopilotMessageNotFoundDataType
|
| CopilotMessageNotFoundDataType
|
||||||
@@ -280,6 +274,8 @@ export enum ErrorNames {
|
|||||||
AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
|
AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
|
||||||
BLOB_NOT_FOUND = 'BLOB_NOT_FOUND',
|
BLOB_NOT_FOUND = 'BLOB_NOT_FOUND',
|
||||||
BLOB_QUOTA_EXCEEDED = 'BLOB_QUOTA_EXCEEDED',
|
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_CHANGE_WORKSPACE_OWNER = 'CANT_CHANGE_WORKSPACE_OWNER',
|
||||||
CANT_UPDATE_LIFETIME_SUBSCRIPTION = 'CANT_UPDATE_LIFETIME_SUBSCRIPTION',
|
CANT_UPDATE_LIFETIME_SUBSCRIPTION = 'CANT_UPDATE_LIFETIME_SUBSCRIPTION',
|
||||||
COPILOT_ACTION_TAKEN = 'COPILOT_ACTION_TAKEN',
|
COPILOT_ACTION_TAKEN = 'COPILOT_ACTION_TAKEN',
|
||||||
@@ -455,7 +451,7 @@ export interface ListUserInput {
|
|||||||
|
|
||||||
export interface ManageUserInput {
|
export interface ManageUserInput {
|
||||||
/** User email */
|
/** User email */
|
||||||
email: Scalars['String']['input'];
|
email: InputMaybe<Scalars['String']['input']>;
|
||||||
/** User name */
|
/** User name */
|
||||||
name: InputMaybe<Scalars['String']['input']>;
|
name: InputMaybe<Scalars['String']['input']>;
|
||||||
}
|
}
|
||||||
@@ -468,8 +464,6 @@ export interface MissingOauthQueryParameterDataType {
|
|||||||
export interface Mutation {
|
export interface Mutation {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
acceptInviteById: Scalars['Boolean']['output'];
|
acceptInviteById: Scalars['Boolean']['output'];
|
||||||
addAdminister: Scalars['Boolean']['output'];
|
|
||||||
addToEarlyAccess: Scalars['Int']['output'];
|
|
||||||
addWorkspaceFeature: Scalars['Int']['output'];
|
addWorkspaceFeature: Scalars['Int']['output'];
|
||||||
cancelSubscription: UserSubscription;
|
cancelSubscription: UserSubscription;
|
||||||
changeEmail: UserType;
|
changeEmail: UserType;
|
||||||
@@ -503,10 +497,8 @@ export interface Mutation {
|
|||||||
leaveWorkspace: Scalars['Boolean']['output'];
|
leaveWorkspace: Scalars['Boolean']['output'];
|
||||||
publishPage: WorkspacePage;
|
publishPage: WorkspacePage;
|
||||||
recoverDoc: Scalars['DateTime']['output'];
|
recoverDoc: Scalars['DateTime']['output'];
|
||||||
removeAdminister: Scalars['Boolean']['output'];
|
|
||||||
/** Remove user avatar */
|
/** Remove user avatar */
|
||||||
removeAvatar: RemoveAvatar;
|
removeAvatar: RemoveAvatar;
|
||||||
removeEarlyAccess: Scalars['Int']['output'];
|
|
||||||
removeWorkspaceFeature: Scalars['Int']['output'];
|
removeWorkspaceFeature: Scalars['Int']['output'];
|
||||||
resumeSubscription: UserSubscription;
|
resumeSubscription: UserSubscription;
|
||||||
revoke: Scalars['Boolean']['output'];
|
revoke: Scalars['Boolean']['output'];
|
||||||
@@ -532,6 +524,8 @@ export interface Mutation {
|
|||||||
updateSubscriptionRecurring: UserSubscription;
|
updateSubscriptionRecurring: UserSubscription;
|
||||||
/** Update a user */
|
/** Update a user */
|
||||||
updateUser: UserType;
|
updateUser: UserType;
|
||||||
|
/** update user enabled feature */
|
||||||
|
updateUserFeatures: Array<FeatureType>;
|
||||||
/** Update workspace */
|
/** Update workspace */
|
||||||
updateWorkspace: WorkspaceType;
|
updateWorkspace: WorkspaceType;
|
||||||
/** Upload user avatar */
|
/** Upload user avatar */
|
||||||
@@ -545,15 +539,6 @@ export interface MutationAcceptInviteByIdArgs {
|
|||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MutationAddAdministerArgs {
|
|
||||||
email: Scalars['String']['input'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MutationAddToEarlyAccessArgs {
|
|
||||||
email: Scalars['String']['input'];
|
|
||||||
type: EarlyAccessType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MutationAddWorkspaceFeatureArgs {
|
export interface MutationAddWorkspaceFeatureArgs {
|
||||||
feature: FeatureType;
|
feature: FeatureType;
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
@@ -649,15 +634,6 @@ export interface MutationRecoverDocArgs {
|
|||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MutationRemoveAdministerArgs {
|
|
||||||
email: Scalars['String']['input'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MutationRemoveEarlyAccessArgs {
|
|
||||||
email: Scalars['String']['input'];
|
|
||||||
type: EarlyAccessType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MutationRemoveWorkspaceFeatureArgs {
|
export interface MutationRemoveWorkspaceFeatureArgs {
|
||||||
feature: FeatureType;
|
feature: FeatureType;
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
@@ -753,6 +729,11 @@ export interface MutationUpdateUserArgs {
|
|||||||
input: ManageUserInput;
|
input: ManageUserInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MutationUpdateUserFeaturesArgs {
|
||||||
|
features: Array<FeatureType>;
|
||||||
|
id: Scalars['String']['input'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface MutationUpdateWorkspaceArgs {
|
export interface MutationUpdateWorkspaceArgs {
|
||||||
input: UpdateWorkspaceInput;
|
input: UpdateWorkspaceInput;
|
||||||
}
|
}
|
||||||
@@ -804,7 +785,6 @@ export interface Query {
|
|||||||
collectAllBlobSizes: WorkspaceBlobSizes;
|
collectAllBlobSizes: WorkspaceBlobSizes;
|
||||||
/** Get current user */
|
/** Get current user */
|
||||||
currentUser: Maybe<UserType>;
|
currentUser: Maybe<UserType>;
|
||||||
earlyAccessUsers: Array<UserType>;
|
|
||||||
error: ErrorDataUnion;
|
error: ErrorDataUnion;
|
||||||
/** send workspace invitation */
|
/** send workspace invitation */
|
||||||
getInviteInfo: InvitationType;
|
getInviteInfo: InvitationType;
|
||||||
@@ -932,6 +912,8 @@ export interface SameSubscriptionRecurringDataType {
|
|||||||
|
|
||||||
export interface ServerConfigType {
|
export interface ServerConfigType {
|
||||||
__typename?: 'ServerConfigType';
|
__typename?: 'ServerConfigType';
|
||||||
|
/** Features for user that can be configured */
|
||||||
|
availableUserFeatures: Array<FeatureType>;
|
||||||
/** server base url */
|
/** server base url */
|
||||||
baseUrl: Scalars['String']['output'];
|
baseUrl: Scalars['String']['output'];
|
||||||
/** credentials requirement */
|
/** credentials requirement */
|
||||||
@@ -1254,13 +1236,28 @@ export interface TokenType {
|
|||||||
token: Scalars['String']['output'];
|
token: Scalars['String']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddToAdminMutationVariables = Exact<{
|
export type AdminServerConfigQueryVariables = Exact<{ [key: string]: never }>;
|
||||||
email: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type AddToAdminMutation = {
|
export type AdminServerConfigQuery = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Query';
|
||||||
addAdminister: boolean;
|
serverConfig: {
|
||||||
|
__typename?: 'ServerConfigType';
|
||||||
|
version: string;
|
||||||
|
baseUrl: string;
|
||||||
|
name: string;
|
||||||
|
features: Array<ServerFeature>;
|
||||||
|
type: ServerDeploymentType;
|
||||||
|
initialized: boolean;
|
||||||
|
availableUserFeatures: Array<FeatureType>;
|
||||||
|
credentialsRequirement: {
|
||||||
|
__typename?: 'CredentialsRequirementType';
|
||||||
|
password: {
|
||||||
|
__typename?: 'PasswordLimitsType';
|
||||||
|
minLength: number;
|
||||||
|
maxLength: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteBlobMutationVariables = Exact<{
|
export type DeleteBlobMutationVariables = Exact<{
|
||||||
@@ -1440,48 +1437,6 @@ export type DeleteWorkspaceMutation = {
|
|||||||
deleteWorkspace: boolean;
|
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<{
|
export type ForkCopilotSessionMutationVariables = Exact<{
|
||||||
options: ForkChatSessionInput;
|
options: ForkChatSessionInput;
|
||||||
}>;
|
}>;
|
||||||
@@ -1956,17 +1911,6 @@ export type ListUsersQuery = {
|
|||||||
hasPassword: boolean | null;
|
hasPassword: boolean | null;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
avatarUrl: string | null;
|
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;
|
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 RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
export type RemoveAvatarMutation = {
|
export type RemoveAvatarMutation = {
|
||||||
@@ -2154,7 +2089,6 @@ export type ServerConfigQuery = {
|
|||||||
name: string;
|
name: string;
|
||||||
features: Array<ServerFeature>;
|
features: Array<ServerFeature>;
|
||||||
type: ServerDeploymentType;
|
type: ServerDeploymentType;
|
||||||
initialized: boolean;
|
|
||||||
credentialsRequirement: {
|
credentialsRequirement: {
|
||||||
__typename?: 'CredentialsRequirementType';
|
__typename?: 'CredentialsRequirementType';
|
||||||
password: {
|
password: {
|
||||||
@@ -2197,6 +2131,16 @@ export type SubscriptionQuery = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateAccountFeaturesMutationVariables = Exact<{
|
||||||
|
userId: Scalars['String']['input'];
|
||||||
|
features: Array<FeatureType> | FeatureType;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type UpdateAccountFeaturesMutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
updateUserFeatures: Array<FeatureType>;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateAccountMutationVariables = Exact<{
|
export type UpdateAccountMutationVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
input: ManageUserInput;
|
input: ManageUserInput;
|
||||||
@@ -2422,6 +2366,11 @@ export type WorkspaceQuotaQuery = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Queries =
|
export type Queries =
|
||||||
|
| {
|
||||||
|
name: 'adminServerConfigQuery';
|
||||||
|
variables: AdminServerConfigQueryVariables;
|
||||||
|
response: AdminServerConfigQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'listBlobsQuery';
|
name: 'listBlobsQuery';
|
||||||
variables: ListBlobsQueryVariables;
|
variables: ListBlobsQueryVariables;
|
||||||
@@ -2432,11 +2381,6 @@ export type Queries =
|
|||||||
variables: CopilotQuotaQueryVariables;
|
variables: CopilotQuotaQueryVariables;
|
||||||
response: CopilotQuotaQuery;
|
response: CopilotQuotaQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'earlyAccessUsersQuery';
|
|
||||||
variables: EarlyAccessUsersQueryVariables;
|
|
||||||
response: EarlyAccessUsersQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'getCopilotHistoriesQuery';
|
name: 'getCopilotHistoriesQuery';
|
||||||
variables: GetCopilotHistoriesQueryVariables;
|
variables: GetCopilotHistoriesQueryVariables;
|
||||||
@@ -2614,11 +2558,6 @@ export type Queries =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Mutations =
|
export type Mutations =
|
||||||
| {
|
|
||||||
name: 'addToAdminMutation';
|
|
||||||
variables: AddToAdminMutationVariables;
|
|
||||||
response: AddToAdminMutation;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'deleteBlobMutation';
|
name: 'deleteBlobMutation';
|
||||||
variables: DeleteBlobMutationVariables;
|
variables: DeleteBlobMutationVariables;
|
||||||
@@ -2699,16 +2638,6 @@ export type Mutations =
|
|||||||
variables: DeleteWorkspaceMutationVariables;
|
variables: DeleteWorkspaceMutationVariables;
|
||||||
response: DeleteWorkspaceMutation;
|
response: DeleteWorkspaceMutation;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'addToEarlyAccessMutation';
|
|
||||||
variables: AddToEarlyAccessMutationVariables;
|
|
||||||
response: AddToEarlyAccessMutation;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: 'removeEarlyAccessMutation';
|
|
||||||
variables: RemoveEarlyAccessMutationVariables;
|
|
||||||
response: RemoveEarlyAccessMutation;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'forkCopilotSessionMutation';
|
name: 'forkCopilotSessionMutation';
|
||||||
variables: ForkCopilotSessionMutationVariables;
|
variables: ForkCopilotSessionMutationVariables;
|
||||||
@@ -2729,11 +2658,6 @@ export type Mutations =
|
|||||||
variables: RecoverDocMutationVariables;
|
variables: RecoverDocMutationVariables;
|
||||||
response: RecoverDocMutation;
|
response: RecoverDocMutation;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'removeAdminMutation';
|
|
||||||
variables: RemoveAdminMutationVariables;
|
|
||||||
response: RemoveAdminMutation;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'removeAvatarMutation';
|
name: 'removeAvatarMutation';
|
||||||
variables: RemoveAvatarMutationVariables;
|
variables: RemoveAvatarMutationVariables;
|
||||||
@@ -2784,6 +2708,11 @@ export type Mutations =
|
|||||||
variables: SetWorkspacePublicByIdMutationVariables;
|
variables: SetWorkspacePublicByIdMutationVariables;
|
||||||
response: SetWorkspacePublicByIdMutation;
|
response: SetWorkspacePublicByIdMutation;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'updateAccountFeaturesMutation';
|
||||||
|
variables: UpdateAccountFeaturesMutationVariables;
|
||||||
|
response: UpdateAccountFeaturesMutation;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'updateAccountMutation';
|
name: 'updateAccountMutation';
|
||||||
variables: UpdateAccountMutationVariables;
|
variables: UpdateAccountMutationVariables;
|
||||||
|
|||||||
Reference in New Issue
Block a user