实现用户注销和删除系统
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AdminController } from './admin.controller';
|
import { AdminController } from './admin.controller';
|
||||||
import { AdminUserController } from './controller/admin-user.controller';
|
import { AdminUserController } from './controller/admin-user.controller';
|
||||||
import { AdminUserService } from './service/admin-user.service';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { User } from 'src/user/entities/user.entity';
|
import { User } from 'src/user/entities/user.entity';
|
||||||
import { UserModule } from 'src/user/user.module';
|
import { UserModule } from 'src/user/user.module';
|
||||||
@@ -35,8 +34,5 @@ import { BlogModule } from 'src/blog/blog.module';
|
|||||||
AdminWebResourceController,
|
AdminWebResourceController,
|
||||||
AdminWebBlogController,
|
AdminWebBlogController,
|
||||||
],
|
],
|
||||||
providers: [
|
|
||||||
AdminUserService,
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class AdminModule { }
|
export class AdminModule { }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CreateDto } from "../dto/admin-user/create.dto";
|
|||||||
import { UserService } from "src/user/user.service";
|
import { UserService } from "src/user/user.service";
|
||||||
import { UpdateDto } from "../dto/admin-user/update.dto";
|
import { UpdateDto } from "../dto/admin-user/update.dto";
|
||||||
import { UpdatePasswordDto } from "../dto/admin-user/update-password.dto";
|
import { UpdatePasswordDto } from "../dto/admin-user/update-password.dto";
|
||||||
|
import { RemoveUserDto } from "../dto/admin-user/remove.dto";
|
||||||
|
|
||||||
@Controller('admin/user')
|
@Controller('admin/user')
|
||||||
export class AdminUserController {
|
export class AdminUserController {
|
||||||
@@ -53,8 +54,9 @@ export class AdminUserController {
|
|||||||
@Delete(':userId')
|
@Delete(':userId')
|
||||||
async delete(
|
async delete(
|
||||||
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
|
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
|
||||||
|
@Query() dto: RemoveUserDto,
|
||||||
) {
|
) {
|
||||||
return this.userService.delete(userId);
|
return this.userService.delete(userId, dto.soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':userId/password')
|
@Post(':userId/password')
|
||||||
|
|||||||
8
tone-page-server/src/admin/dto/admin-user/remove.dto.ts
Normal file
8
tone-page-server/src/admin/dto/admin-user/remove.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Transform } from "class-transformer";
|
||||||
|
import { IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
export class RemoveUserDto {
|
||||||
|
@Transform(({ value }) => value === 'true')
|
||||||
|
@IsBoolean({ message: '需指定删除类型' })
|
||||||
|
soft: boolean;
|
||||||
|
}
|
||||||
@@ -51,12 +51,23 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(userId: string): Promise<void> {
|
async delete(userId: string, soft: boolean) {
|
||||||
const existingUser = await this.userRepository.findOne({ where: { userId } });
|
const existingUser = await this.userRepository.findOne({ where: { userId }, withDeleted: true });
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw new BadRequestException('User not found');
|
throw new BadRequestException('用户不存在');
|
||||||
}
|
}
|
||||||
await this.userRepository.softDelete(existingUser.userId);
|
|
||||||
|
if (existingUser.deletedAt && soft) {
|
||||||
|
throw new BadRequestException('账户已注销,不得重复操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingUser.deletedAt && !soft) {
|
||||||
|
throw new BadRequestException('账号未注销,请先注销再执行删除操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
return soft
|
||||||
|
? await this.userRepository.softDelete(existingUser.userId)
|
||||||
|
: await this.userRepository.delete(existingUser.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
hashPassword(password: string, salt: string): string {
|
hashPassword(password: string, salt: string): string {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function CreateUserEditor({ children, onRefresh }: CreateUserEditorProps)
|
|||||||
<Label htmlFor="password">密码</Label>
|
<Label htmlFor="password">密码</Label>
|
||||||
<Input id="password" name="password" />
|
<Input id="password" name="password" />
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit">保存</Button>
|
<Button type="submit">创建</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DrawerFooter className="pt-2">
|
<DrawerFooter className="pt-2">
|
||||||
|
|||||||
@@ -27,17 +27,17 @@ import {
|
|||||||
} from "@/components/ui/alert"
|
} from "@/components/ui/alert"
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
|
||||||
export function UserInfoEditor({
|
export function UserInfoEditor({
|
||||||
onClose,
|
onClose,
|
||||||
onUserUpdate,
|
onUserUpdate,
|
||||||
onUserDelete,
|
onUserSoftDelete,
|
||||||
userId,
|
userId,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
onUserUpdate: (user: User) => void,
|
onUserUpdate: (user: User) => void,
|
||||||
onUserDelete: (userId: string) => void,
|
onUserSoftDelete: (userId: string) => void,
|
||||||
userId: string
|
userId: string
|
||||||
}) {
|
}) {
|
||||||
const { user, isLoading, error } = useUser(userId);
|
const { user, isLoading, error } = useUser(userId);
|
||||||
@@ -65,12 +65,12 @@ export function UserInfoEditor({
|
|||||||
const handleRemove = async (userId: string) => {
|
const handleRemove = async (userId: string) => {
|
||||||
try {
|
try {
|
||||||
setRemoveLoading(true);
|
setRemoveLoading(true);
|
||||||
await AdminApi.user.remove(userId);
|
await AdminApi.user.remove(userId, true);
|
||||||
toast.success("删除成功");
|
toast.success("注销成功");
|
||||||
onUserDelete(userId);
|
onUserSoftDelete(userId);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error((error as Error).message || "删除失败");
|
toast.error((error as Error).message || "注销失败");
|
||||||
} finally {
|
} finally {
|
||||||
setRemoveLoading(false);
|
setRemoveLoading(false);
|
||||||
}
|
}
|
||||||
@@ -208,18 +208,18 @@ function ProfileForm({ className, user, onSetPassword, onRemove, passwordDialogO
|
|||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button type="button" variant="destructive" className="flex-1">删除用户</Button>
|
<Button type="button" variant="destructive" className="flex-1">注销</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>是否要删除该用户?</AlertDialogTitle>
|
<AlertDialogTitle>是否要注销该账号?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
该操作无法撤销,这会永久删除该用户的所有数据
|
该操作无法撤销,稍后可通过删除来彻底清理该用户信息
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={() => onRemove(user.userId)}>删除</AlertDialogAction>
|
<AlertDialogAction onClick={() => onRemove(user.userId)}>注销</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ import { useState } from "react";
|
|||||||
import { UserInfoEditor } from "./components/user-info-editor";
|
import { UserInfoEditor } from "./components/user-info-editor";
|
||||||
import { User } from "@/lib/types/user";
|
import { User } from "@/lib/types/user";
|
||||||
import { CreateUserEditor } from "./components/create-user-editor";
|
import { CreateUserEditor } from "./components/create-user-editor";
|
||||||
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||||
|
import { AdminApi } from "@/lib/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { ApiError } from "next/dist/server/api-utils";
|
||||||
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { users, isLoading, error, total, page, pageSize, mutate, refresh } = useUserList();
|
const { users, isLoading, error, total, page, pageSize, mutate, refresh } = useUserList();
|
||||||
const [editorUserId, setEditorUserId] = useState("");
|
const [editorUserId, setEditorUserId] = useState("");
|
||||||
|
|
||||||
const handleUserUpdate = async (newUser: User) => {
|
const handleUserUpdateLocal = async (newUser: User) => {
|
||||||
await mutate(
|
await mutate(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (!data) return data;
|
if (!data) return data;
|
||||||
@@ -31,11 +35,14 @@ export default function Page() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUserDelete = async (userId: string) => {
|
const handleUserDeleteLocal = async (userId: string, soft: boolean) => {
|
||||||
await mutate(
|
await mutate(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (!data) return data;
|
if (!data) return data;
|
||||||
return {
|
return soft ? {
|
||||||
|
...data,
|
||||||
|
items: data.items.map(u => u.userId === userId ? { ...u, deletedAt: new Date().toLocaleString() } : u)
|
||||||
|
} : {
|
||||||
...data,
|
...data,
|
||||||
items: data.items.filter((user) => user.userId !== userId),
|
items: data.items.filter((user) => user.userId !== userId),
|
||||||
};
|
};
|
||||||
@@ -46,6 +53,18 @@ export default function Page() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [deletedUserId, setDeletedUserId] = useState('');
|
||||||
|
const handleUserDelete = async (userId: string) => {
|
||||||
|
try {
|
||||||
|
await AdminApi.user.remove(userId, false);
|
||||||
|
toast.success('删除成功');
|
||||||
|
handleUserDeleteLocal(userId, false);
|
||||||
|
setDeletedUserId('');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error((error as ApiError).message || '删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
@@ -86,7 +105,12 @@ export default function Page() {
|
|||||||
<TableCell>{user.email}</TableCell>
|
<TableCell>{user.email}</TableCell>
|
||||||
<TableCell>{user.phone}</TableCell>
|
<TableCell>{user.phone}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button className="cursor-pointer" variant='outline' size='sm' onClick={() => setEditorUserId(user.userId)}>编辑</Button>
|
{user.deletedAt
|
||||||
|
? <Button className="cursor-pointer" variant='destructive' size='sm'
|
||||||
|
onClick={() => setDeletedUserId(user.userId)}>删除</Button>
|
||||||
|
: <Button className="cursor-pointer" variant='outline' size='sm'
|
||||||
|
onClick={() => setEditorUserId(user.userId)}>编辑</Button>
|
||||||
|
}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
@@ -104,14 +128,29 @@ export default function Page() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table >
|
||||||
|
|
||||||
<UserInfoEditor
|
<UserInfoEditor
|
||||||
onClose={() => setEditorUserId('')}
|
onClose={() => setEditorUserId('')}
|
||||||
userId={editorUserId}
|
userId={editorUserId}
|
||||||
onUserUpdate={handleUserUpdate}
|
onUserUpdate={handleUserUpdateLocal}
|
||||||
onUserDelete={handleUserDelete}
|
onUserSoftDelete={userId => handleUserDeleteLocal(userId, true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AlertDialog open={!!deletedUserId} onOpenChange={o => !o && setDeletedUserId('')}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>是否要彻底删除账号?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
该操作无法撤销,会彻底删除该账号相关连的所有数据
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={() => handleUserDelete(deletedUserId)}>删除</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import fetcher from "../../fetcher";
|
import fetcher from "../../fetcher";
|
||||||
|
|
||||||
export async function remove(userId: string) {
|
export async function remove(userId: string, soft: boolean) {
|
||||||
return fetcher(`/api/admin/user/${userId}`, {
|
return fetcher(`/api/admin/user/${userId}?soft=${soft}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,5 @@ export interface User {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user