feat(admin): add self-host setup and user management page (#7537)

This commit is contained in:
JimmFly
2024-08-13 14:11:03 +08:00
committed by GitHub
parent dc519348c5
commit ccf225c8f9
47 changed files with 2793 additions and 551 deletions

View File

@@ -21,6 +21,7 @@ import {
Throttle,
URLHelper,
} from '../../fundamentals';
import { Admin } from '../common';
import { UserService } from '../user';
import { UserType } from '../user/types';
import { validators } from '../utils/validators';
@@ -291,4 +292,19 @@ export class AuthResolver {
return emailVerifiedAt !== null;
}
@Admin()
@Mutation(() => String, {
description: 'Create change password url',
})
async createChangePasswordUrl(
@Args('userId') userId: string,
@Args('callbackUrl') callbackUrl: string
): Promise<string> {
const token = await this.token.createToken(
TokenType.ChangePassword,
userId
);
return this.url.link(callbackUrl, { token });
}
}

View File

@@ -42,6 +42,10 @@ export class FeatureManagementService {
return this.feature.addUserFeature(userId, FeatureType.Admin, 'Admin user');
}
removeAdmin(userId: string) {
return this.feature.removeUserFeature(userId, FeatureType.Admin);
}
// ======== Early Access ========
async addEarlyAccess(
userId: string,

View File

@@ -57,12 +57,15 @@ export class FeatureManagementResolver {
@Admin()
@Mutation(() => Int)
async removeEarlyAccess(@Args('email') email: string): Promise<number> {
async removeEarlyAccess(
@Args('email') email: string,
@Args({ name: 'type', type: () => EarlyAccessType }) type: EarlyAccessType
): Promise<number> {
const user = await this.users.findUserByEmail(email);
if (!user) {
throw new UserNotFound();
}
return this.feature.removeEarlyAccess(user.id);
return this.feature.removeEarlyAccess(user.id, type);
}
@Admin()
@@ -90,4 +93,18 @@ export class FeatureManagementResolver {
return true;
}
@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;
}
}

View File

@@ -22,6 +22,7 @@ import { validators } from '../utils/validators';
import { UserService } from './service';
import {
DeleteAccount,
ManageUserInput,
RemoveAvatar,
UpdateUserInput,
UserOrLimitedUser,
@@ -174,6 +175,13 @@ export class UserManagementResolver {
private readonly user: UserService
) {}
@Query(() => Int, {
description: 'Get users count',
})
async usersCount(): Promise<number> {
return this.db.user.count();
}
@Query(() => [UserType], {
description: 'List registered users',
})
@@ -208,6 +216,26 @@ export class UserManagementResolver {
return sessionUser(user);
}
@Query(() => UserType, {
name: 'userByEmail',
description: 'Get user by email for admin',
nullable: true,
})
async getUserByEmail(@Args('email') email: string) {
const user = await this.db.user.findUnique({
select: { ...this.user.defaultUserSelect, password: true },
where: {
email,
},
});
if (!user) {
return null;
}
return sessionUser(user);
}
@Mutation(() => UserType, {
description: 'Create a new user',
})
@@ -231,4 +259,36 @@ export class UserManagementResolver {
await this.user.deleteUser(id);
return { success: true };
}
@Mutation(() => UserType, {
description: 'Update a user',
})
async updateUser(
@Args('id') id: string,
@Args('input') input: ManageUserInput
): Promise<UserType> {
const user = await this.db.user.findUnique({
select: { ...this.user.defaultUserSelect, password: true },
where: { id },
});
if (!user) {
throw new UserNotFound();
}
validators.assertValidEmail(input.email);
if (input.email !== user.email) {
const exists = await this.db.user.findFirst({
where: { email: input.email },
});
if (exists) {
throw new Error('Email already exists');
}
}
return sessionUser(
await this.user.updateUser(user.id, {
name: input.name,
email: input.email,
})
);
}
}

View File

@@ -83,6 +83,15 @@ export class UpdateUserInput implements Partial<User> {
name?: string;
}
@InputType()
export class ManageUserInput {
@Field({ description: 'User name', nullable: true })
name?: string;
@Field({ description: 'User email' })
email!: string;
}
declare module '../../fundamentals/event/def' {
interface UserEvents {
admin: {

View File

@@ -396,6 +396,14 @@ input ListUserInput {
skip: Int = 0
}
input ManageUserInput {
"""User email"""
email: String!
"""User name"""
name: String
}
type MissingOauthQueryParameterDataType {
name: String!
}
@@ -412,6 +420,9 @@ type Mutation {
"""Cleanup sessions"""
cleanupCopilotSession(options: DeleteSessionInput!): [String!]!
"""Create change password url"""
createChangePasswordUrl(callbackUrl: String!, userId: String!): String!
"""Create a subscription checkout link of stripe"""
createCheckoutSession(input: CreateCheckoutSessionInput!): String!
@@ -445,10 +456,11 @@ type Mutation {
leaveWorkspace(sendLeaveMail: Boolean, workspaceId: String!, workspaceName: String!): Boolean!
publishPage(mode: PublicPageMode = Page, pageId: String!, workspaceId: String!): WorkspacePage!
recoverDoc(guid: String!, timestamp: DateTime!, workspaceId: String!): DateTime!
removeAdminister(email: String!): Boolean!
"""Remove user avatar"""
removeAvatar: RemoveAvatar!
removeEarlyAccess(email: String!): Int!
removeEarlyAccess(email: String!, type: EarlyAccessType!): Int!
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
resumeSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription!
revoke(userId: String!, workspaceId: String!): Boolean!
@@ -474,6 +486,9 @@ type Mutation {
updateRuntimeConfigs(updates: JSONObject!): [ServerRuntimeConfigType!]!
updateSubscriptionRecurring(idempotencyKey: String!, plan: SubscriptionPlan = Pro, recurring: SubscriptionRecurring!): UserSubscription!
"""Update a user"""
updateUser(id: String!, input: ManageUserInput!): UserType!
"""Update workspace"""
updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType!
@@ -544,12 +559,18 @@ type Query {
"""Get user by email"""
user(email: String!): UserOrLimitedUser
"""Get user by email for admin"""
userByEmail(email: String!): UserType
"""Get user by id"""
userById(id: String!): UserType!
"""List registered users"""
users(filter: ListUserInput!): [UserType!]!
"""Get users count"""
usersCount: Int!
"""Get workspace by id"""
workspace(id: String!): WorkspaceType!