0.2.0.0
This commit is contained in:
@@ -0,0 +1,535 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { format } from "date-fns"
|
||||
import { zhCN } from "date-fns/locale"
|
||||
import { Users, TreePine, UserPlus, ArrowLeft, ChevronDown, ChevronRight, Shield, Ban, Check, LogIn } from "lucide-react"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
interface Collaborator {
|
||||
id: string
|
||||
name: string | null
|
||||
email: string
|
||||
role: string
|
||||
}
|
||||
|
||||
interface TreeOwner {
|
||||
id: string
|
||||
name: string | null
|
||||
email: string
|
||||
}
|
||||
|
||||
interface TreeInfo {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: string
|
||||
memberCount: number
|
||||
collaborators: Collaborator[]
|
||||
}
|
||||
|
||||
interface CollaboratingTree {
|
||||
id: string
|
||||
name: string
|
||||
role: string
|
||||
owner: TreeOwner
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
id: string
|
||||
email: string
|
||||
name: string | null
|
||||
avatar: string | null
|
||||
isAdmin: boolean
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
treeCount: number
|
||||
totalMembers: number
|
||||
trees: TreeInfo[]
|
||||
collaboratingTrees: CollaboratingTree[]
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
totalUsers: number
|
||||
totalTrees: number
|
||||
totalMembers: number
|
||||
}
|
||||
|
||||
interface LoginLog {
|
||||
id: string
|
||||
userId: string
|
||||
email: string
|
||||
userName: string | null
|
||||
ip: string | null
|
||||
userAgent: string | null
|
||||
success: boolean
|
||||
message: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export default function AdminPage() {
|
||||
const { data: session, status } = useSession()
|
||||
const router = useRouter()
|
||||
const [users, setUsers] = useState<UserInfo[]>([])
|
||||
const [stats, setStats] = useState<Stats | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [expandedUsers, setExpandedUsers] = useState<Set<string>>(new Set())
|
||||
const [updating, setUpdating] = useState<string | null>(null)
|
||||
const [loginLogs, setLoginLogs] = useState<LoginLog[]>([])
|
||||
const [logsLoading, setLogsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
router.push("/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
if (status === "authenticated") {
|
||||
fetchUsers()
|
||||
fetchLoginLogs()
|
||||
}
|
||||
}, [status, router])
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch("/api/admin/users")
|
||||
if (!response.ok) {
|
||||
throw new Error("获取数据失败")
|
||||
}
|
||||
const data = await response.json()
|
||||
setUsers(data.users)
|
||||
setStats(data.stats)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "未知错误")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchLoginLogs = async () => {
|
||||
try {
|
||||
setLogsLoading(true)
|
||||
const response = await fetch("/api/admin/login-logs?limit=100")
|
||||
if (!response.ok) {
|
||||
// 如果获取失败,设置为空数组,不报错
|
||||
setLoginLogs([])
|
||||
return
|
||||
}
|
||||
const data = await response.json()
|
||||
setLoginLogs(data.logs || [])
|
||||
} catch (err) {
|
||||
console.error("获取登录日志失败:", err)
|
||||
setLoginLogs([])
|
||||
} finally {
|
||||
setLogsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleUserExpand = (userId: string) => {
|
||||
setExpandedUsers((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(userId)) {
|
||||
newSet.delete(userId)
|
||||
} else {
|
||||
newSet.add(userId)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return format(new Date(dateString), "yyyy年MM月dd日 HH:mm", { locale: zhCN })
|
||||
}
|
||||
|
||||
const toggleUserStatus = async (userId: string, currentStatus: boolean) => {
|
||||
try {
|
||||
setUpdating(userId)
|
||||
const response = await fetch(`/api/admin/users/${userId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ isActive: !currentStatus }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.error || "操作失败")
|
||||
}
|
||||
|
||||
// 更新本地状态
|
||||
setUsers((prev) =>
|
||||
prev.map((user) =>
|
||||
user.id === userId ? { ...user, isActive: !currentStatus } : user
|
||||
)
|
||||
)
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : "操作失败")
|
||||
} finally {
|
||||
setUpdating(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (status === "loading" || loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Skeleton className="h-32" />
|
||||
<Skeleton className="h-32" />
|
||||
<Skeleton className="h-32" />
|
||||
</div>
|
||||
<Skeleton className="h-96" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Card className="border-destructive">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-destructive">错误</CardTitle>
|
||||
<CardDescription>{error}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.push("/")}>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">用户管理</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">总用户数</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.totalUsers}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">总家族数</CardTitle>
|
||||
<TreePine className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.totalTrees}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">总成员数</CardTitle>
|
||||
<UserPlus className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.totalMembers}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs defaultValue="users" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="users" className="gap-2">
|
||||
<Users className="h-4 w-4" />
|
||||
用户列表
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="logs" className="gap-2">
|
||||
<LogIn className="h-4 w-4" />
|
||||
登录日志
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 用户列表 Tab */}
|
||||
<TabsContent value="users">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>用户列表</CardTitle>
|
||||
<CardDescription>所有注册用户及其创建的家族信息</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-10"></TableHead>
|
||||
<TableHead>用户</TableHead>
|
||||
<TableHead>邮箱</TableHead>
|
||||
<TableHead className="text-center">状态</TableHead>
|
||||
<TableHead className="text-center">家族数</TableHead>
|
||||
<TableHead className="text-center">成员数</TableHead>
|
||||
<TableHead>注册时间</TableHead>
|
||||
<TableHead className="text-center">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<React.Fragment key={user.id}>
|
||||
<TableRow
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleUserExpand(user.id)}
|
||||
>
|
||||
<TableCell>
|
||||
{(user.trees.length > 0 || user.collaboratingTrees.length > 0) && (
|
||||
expandedUsers.has(user.id) ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
{user.name || "未设置"}
|
||||
{user.isAdmin && (
|
||||
<span title="管理员">
|
||||
<Shield className="h-4 w-4 text-amber-500" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{user.isActive ? (
|
||||
<span className="inline-flex items-center gap-1 text-green-600">
|
||||
<Check className="h-4 w-4" />
|
||||
启用
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-red-500">
|
||||
<Ban className="h-4 w-4" />
|
||||
停用
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{user.treeCount}</TableCell>
|
||||
<TableCell className="text-center">{user.totalMembers}</TableCell>
|
||||
<TableCell>{formatDate(user.createdAt)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{!user.isAdmin && (
|
||||
<Button
|
||||
variant={user.isActive ? "destructive" : "default"}
|
||||
size="sm"
|
||||
disabled={updating === user.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleUserStatus(user.id, user.isActive)
|
||||
}}
|
||||
>
|
||||
{updating === user.id
|
||||
? "处理中..."
|
||||
: user.isActive
|
||||
? "停用"
|
||||
: "启用"}
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/* 展开的家族列表和协作信息 */}
|
||||
{expandedUsers.has(user.id) && (user.trees.length > 0 || user.collaboratingTrees.length > 0) && (
|
||||
<TableRow key={`${user.id}-trees`}>
|
||||
<TableCell colSpan={8} className="bg-muted/50 p-0">
|
||||
<div className="px-8 py-3 space-y-4">
|
||||
{/* 创建的家族 */}
|
||||
{user.trees.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">创建的家族</h4>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>家族名称</TableHead>
|
||||
<TableHead className="text-center">成员数</TableHead>
|
||||
<TableHead>协作者</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{user.trees.map((tree) => (
|
||||
<TableRow key={tree.id}>
|
||||
<TableCell className="font-medium">
|
||||
{tree.name}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{tree.memberCount}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{tree.collaborators.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tree.collaborators.map((c) => (
|
||||
<span
|
||||
key={c.id}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-700"
|
||||
title={c.email}
|
||||
>
|
||||
{c.name || c.email}
|
||||
<span className="ml-1 text-blue-500">
|
||||
({c.role === 'EDITOR' ? '编辑' : '查看'})
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">无</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(tree.createdAt)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 协作的家族 */}
|
||||
{user.collaboratingTrees.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">协作的家族</h4>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>家族名称</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>所有者</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{user.collaboratingTrees.map((tree) => (
|
||||
<TableRow key={tree.id}>
|
||||
<TableCell className="font-medium">
|
||||
{tree.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs ${
|
||||
tree.role === 'EDITOR'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{tree.role === 'EDITOR' ? '编辑者' : '查看者'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span title={tree.owner.email}>
|
||||
{tree.owner.name || tree.owner.email}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{users.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
|
||||
暂无用户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 登录日志 Tab */}
|
||||
<TabsContent value="logs">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>登录日志</CardTitle>
|
||||
<CardDescription>用户登录记录,按时间倒序排列</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{logsLoading ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用户</TableHead>
|
||||
<TableHead>邮箱</TableHead>
|
||||
<TableHead className="text-center">状态</TableHead>
|
||||
<TableHead>登录时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loginLogs.map((log) => (
|
||||
<TableRow key={log.id}>
|
||||
<TableCell className="font-medium">
|
||||
{log.userName || "未设置"}
|
||||
</TableCell>
|
||||
<TableCell>{log.email}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{log.success ? (
|
||||
<span className="inline-flex items-center gap-1 text-green-600">
|
||||
<Check className="h-4 w-4" />
|
||||
成功
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-red-500">
|
||||
<Ban className="h-4 w-4" />
|
||||
失败
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(log.createdAt)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{loginLogs.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
|
||||
暂无登录记录
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
// 检查是否为管理员
|
||||
async function checkAdmin(userId: string) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { isAdmin: true }
|
||||
})
|
||||
return user?.isAdmin === true
|
||||
}
|
||||
|
||||
// 获取登录日志
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "未授权" }, { status: 401 })
|
||||
}
|
||||
|
||||
// 检查管理员权限
|
||||
const isAdmin = await checkAdmin(session.user.id)
|
||||
if (!isAdmin) {
|
||||
return NextResponse.json({ error: "无权限访问" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
const limit = parseInt(searchParams.get("limit") || "100")
|
||||
const page = parseInt(searchParams.get("page") || "1")
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// 获取登录日志总数
|
||||
const total = await prisma.loginLog.count()
|
||||
|
||||
// 获取登录日志列表
|
||||
const logs = await prisma.loginLog.findMany({
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: limit,
|
||||
skip,
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
email: true,
|
||||
userName: true,
|
||||
ip: true,
|
||||
userAgent: true,
|
||||
success: true,
|
||||
message: true,
|
||||
createdAt: true,
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
logs,
|
||||
pagination: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("获取登录日志失败:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "获取登录日志失败" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
// 检查是否为管理员
|
||||
async function checkAdmin(userId: string) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { isAdmin: true }
|
||||
})
|
||||
return user?.isAdmin === true
|
||||
}
|
||||
|
||||
// 更新用户状态(启用/停用)
|
||||
export async function PATCH(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ userId: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "未授权" }, { status: 401 })
|
||||
}
|
||||
|
||||
// 检查管理员权限
|
||||
const isAdmin = await checkAdmin(session.user.id)
|
||||
if (!isAdmin) {
|
||||
return NextResponse.json({ error: "无权限访问" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { userId } = await params
|
||||
const body = await req.json()
|
||||
const { isActive } = body
|
||||
|
||||
if (typeof isActive !== "boolean") {
|
||||
return NextResponse.json({ error: "参数错误" }, { status: 400 })
|
||||
}
|
||||
|
||||
// 不能停用自己
|
||||
if (userId === session.user.id && !isActive) {
|
||||
return NextResponse.json({ error: "不能停用自己的账户" }, { status: 400 })
|
||||
}
|
||||
|
||||
// 检查目标用户是否存在
|
||||
const targetUser = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true, isAdmin: true }
|
||||
})
|
||||
|
||||
if (!targetUser) {
|
||||
return NextResponse.json({ error: "用户不存在" }, { status: 404 })
|
||||
}
|
||||
|
||||
// 不能停用其他管理员
|
||||
if (targetUser.isAdmin && !isActive) {
|
||||
return NextResponse.json({ error: "不能停用管理员账户" }, { status: 400 })
|
||||
}
|
||||
|
||||
// 更新用户状态
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { isActive },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
isActive: true,
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ user: updatedUser })
|
||||
} catch (error) {
|
||||
console.error("更新用户状态失败:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "更新用户状态失败" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
// 检查是否为管理员
|
||||
async function checkAdmin(userId: string) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { isAdmin: true }
|
||||
})
|
||||
return user?.isAdmin === true
|
||||
}
|
||||
|
||||
// 获取所有用户及其家族统计信息
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "未授权" }, { status: 401 })
|
||||
}
|
||||
|
||||
// 检查管理员权限
|
||||
const isAdmin = await checkAdmin(session.user.id)
|
||||
if (!isAdmin) {
|
||||
return NextResponse.json({ error: "无权限访问" }, { status: 403 })
|
||||
}
|
||||
|
||||
// 获取所有用户及其拥有的家族树
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
isAdmin: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
ownedTrees: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
members: true,
|
||||
},
|
||||
},
|
||||
// 获取该家族树的协作者
|
||||
collaborators: {
|
||||
select: {
|
||||
role: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
// 获取用户作为协作者参与的家族树
|
||||
collaborations: {
|
||||
select: {
|
||||
role: true,
|
||||
tree: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ isAdmin: 'desc' }, // 管理员排在最前面
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
})
|
||||
|
||||
// 格式化数据
|
||||
const formattedUsers = users.map((user) => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
isAdmin: user.isAdmin,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
treeCount: user.ownedTrees.length,
|
||||
totalMembers: user.ownedTrees.reduce((sum: number, tree) => sum + tree._count.members, 0),
|
||||
trees: user.ownedTrees.map((tree) => ({
|
||||
id: tree.id,
|
||||
name: tree.name,
|
||||
createdAt: tree.createdAt,
|
||||
memberCount: tree._count.members,
|
||||
// 协作者列表
|
||||
collaborators: tree.collaborators.map((c) => ({
|
||||
id: c.user.id,
|
||||
name: c.user.name,
|
||||
email: c.user.email,
|
||||
role: c.role,
|
||||
})),
|
||||
})),
|
||||
// 作为协作者参与的家族
|
||||
collaboratingTrees: user.collaborations.map((c) => ({
|
||||
id: c.tree.id,
|
||||
name: c.tree.name,
|
||||
role: c.role,
|
||||
owner: {
|
||||
id: c.tree.owner.id,
|
||||
name: c.tree.owner.name,
|
||||
email: c.tree.owner.email,
|
||||
}
|
||||
})),
|
||||
}))
|
||||
|
||||
// 统计总数
|
||||
const stats = {
|
||||
totalUsers: users.length,
|
||||
totalTrees: formattedUsers.reduce((sum, user) => sum + user.treeCount, 0),
|
||||
totalMembers: formattedUsers.reduce((sum, user) => sum + user.totalMembers, 0),
|
||||
}
|
||||
|
||||
return NextResponse.json({ users: formattedUsers, stats })
|
||||
} catch (error) {
|
||||
console.error("获取用户列表失败:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "获取用户列表失败" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export async function GET(req: NextRequest) {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
isAdmin: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}
|
||||
|
||||
+14
-2
@@ -4,7 +4,8 @@ import { SiteHeader } from "@/components/site-header"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Download, Upload, AlertTriangle, RefreshCw, FileText, Bell, History, Database, User as UserIcon, Settings, User } from "lucide-react"
|
||||
import { Download, Upload, AlertTriangle, RefreshCw, FileText, Bell, History, Database, User as UserIcon, Settings, User, Users } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { useRef, useState, useEffect } from "react"
|
||||
import { exportToGedcom, importFromGedcom } from "@/lib/gedcom"
|
||||
@@ -22,6 +23,7 @@ interface UserProfile {
|
||||
id: string
|
||||
email: string
|
||||
name: string | null
|
||||
isAdmin: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
ownedTreesCount: number
|
||||
@@ -102,7 +104,17 @@ export default function SettingsPage() {
|
||||
<div className="min-h-screen bg-background flex flex-col font-sans">
|
||||
<SiteHeader />
|
||||
<main className="container mx-auto py-8 px-4 md:px-6 flex-1 max-w-6xl">
|
||||
<h1 className="text-3xl font-serif font-bold mb-8">系统设置</h1>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-3xl font-serif font-bold">系统设置</h1>
|
||||
{userProfile?.isAdmin && (
|
||||
<Link href="/admin">
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Users className="h-4 w-4" />
|
||||
用户管理
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 用户信息和密码修改 - 两列布局 */}
|
||||
|
||||
Reference in New Issue
Block a user