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: {