diff --git a/package-lock.json b/package-lock.json
index 088719e..3493f92 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -36,8 +36,8 @@
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@types/crypto-js": "^4.2.2",
- "@types/node": "^20",
- "@types/react": "^18",
+ "@types/node": "^20.17.19",
+ "@types/react": "^18.3.18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.7",
diff --git a/package.json b/package.json
index 1415b3e..6259b57 100644
--- a/package.json
+++ b/package.json
@@ -43,8 +43,8 @@
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@types/crypto-js": "^4.2.2",
- "@types/node": "^20",
- "@types/react": "^18",
+ "@types/node": "^20.17.19",
+ "@types/react": "^18.3.18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.7",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index f68d0d8..a8516a1 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -80,6 +80,7 @@ model AccessToken {
id String @id @default(cuid())
token String @unique
expiresAt DateTime
+ error String?
userId String
user User @relation(fields: [userId], references: [id])
@@ -87,6 +88,9 @@ model AccessToken {
clientId String
client Client @relation(fields: [clientId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
@@map("access_tokens")
}
diff --git a/src/app/(admin)/admin/clients/[id]/page.tsx b/src/app/(admin)/admin/clients/[id]/page.tsx
new file mode 100644
index 0000000..fa3f66b
--- /dev/null
+++ b/src/app/(admin)/admin/clients/[id]/page.tsx
@@ -0,0 +1,237 @@
+"use client";
+
+import Link from "next/link";
+import { notFound, redirect } from "next/navigation";
+import type { ExtendedAccessToken, ExtendedClient } from "@/types";
+import { ArrowLeft } from "lucide-react";
+
+import { prisma } from "@/lib/prisma";
+import { getCurrentUser } from "@/lib/session";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+async function getClientDetails(id: string) {
+ const client = await prisma.client.findUnique({
+ where: { id },
+ include: {
+ user: {
+ select: {
+ username: true,
+ email: true,
+ },
+ },
+ accessTokens: {
+ where: {
+ expiresAt: {
+ gt: new Date(),
+ },
+ },
+ include: {
+ client: {
+ select: {
+ name: true,
+ },
+ },
+ user: {
+ select: {
+ username: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ if (!client) {
+ return null;
+ }
+
+ // 获取最近30天的授权统计
+ const thirtyDaysAgo = new Date();
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
+
+ const authCount = await prisma.accessToken.count({
+ where: {
+ clientId: id,
+ expiresAt: {
+ gte: thirtyDaysAgo,
+ },
+ },
+ });
+
+ return {
+ ...client,
+ authCount,
+ } as ExtendedClient;
+}
+
+export default async function ClientDetailsPage({
+ params,
+}: {
+ params: { id: string };
+}) {
+ const user = await getCurrentUser();
+ if (!user || user.role !== "ADMIN") {
+ redirect("/dashboard");
+ }
+
+ const client = await getClientDetails(params.id);
+ if (!client) {
+ notFound();
+ }
+
+ return (
+
+
+
+
+
+
+
{client.name}
+
+ {client.enabled ? "启用" : "禁用"}
+
+
+
+
+
+
+
+ 应用信息
+ 应用的基本信息
+
+
+
+
+
-
+ Client ID
+
+ - {client.clientId}
+
+
+
-
+ Client Secret
+
+ - {client.clientSecret}
+
+
+
-
+ 回调地址
+
+ - {client.redirectUri}
+
+
+
+
-
+ 创建者
+
+ - {client.user.username}
+
+
+
-
+ 创建时间
+
+ -
+ {new Date(client.createdAt).toLocaleString()}
+
+
+
+
+
+
+
+
+ 使用统计
+ 应用的使用情况统计
+
+
+
+
+
-
+ 最近30天授权次数
+
+ - {client.authCount}
+
+
+
-
+ 当前活跃令牌
+
+ -
+ {client.accessTokens?.length || 0}
+
+
+
+
+
+
+
+
+ 活跃令牌
+ 当前有效的访问令牌
+
+
+
+
+
+ 令牌ID
+ 创建时间
+ 过期时间
+
+
+
+ {(client.accessTokens || []).map(
+ (token: ExtendedAccessToken) => (
+
+ {token.id}
+
+ {new Date(token.createdAt).toLocaleString()}
+
+
+ {new Date(token.expiresAt).toLocaleString()}
+
+
+ ),
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(admin)/admin/clients/page.tsx b/src/app/(admin)/admin/clients/page.tsx
new file mode 100644
index 0000000..11a8d1f
--- /dev/null
+++ b/src/app/(admin)/admin/clients/page.tsx
@@ -0,0 +1,147 @@
+import Link from "next/link";
+import { redirect } from "next/navigation";
+import { Search } from "lucide-react";
+
+import { prisma } from "@/lib/prisma";
+import { getCurrentUser } from "@/lib/session";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+async function getClients(search?: string) {
+ const where = search
+ ? {
+ OR: [
+ { name: { contains: search } },
+ { clientId: { contains: search } },
+ { description: { contains: search } },
+ ],
+ }
+ : {};
+
+ const clients = await prisma.client.findMany({
+ where,
+ include: {
+ user: {
+ select: {
+ username: true,
+ email: true,
+ },
+ },
+ },
+ orderBy: {
+ createdAt: "desc",
+ },
+ });
+
+ return clients;
+}
+
+export default async function ClientsPage({
+ searchParams,
+}: {
+ searchParams: { search?: string };
+}) {
+ const user = await getCurrentUser();
+ if (!user || user.role !== "ADMIN") {
+ redirect("/dashboard");
+ }
+
+ const clients = await getClients(searchParams.search);
+
+ return (
+
+
+
+
+
+ 应用管理
+ 查看和管理系统中的所有应用
+
+
+
+
+
+
+
+
+ 应用名称
+ 创建者
+ Client ID
+ 回调地址
+ 创建时间
+ 状态
+ 操作
+
+
+
+ {clients.map((client) => (
+
+
+
+ {client.logo && (
+

+ )}
+ {client.name}
+
+
+ {client.user.username}
+ {client.clientId}
+
+ {client.redirectUri}
+
+
+ {new Date(client.createdAt).toLocaleString()}
+
+
+
+ {client.enabled ? "启用" : "禁用"}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/app/(admin)/admin/logs/page.tsx b/src/app/(admin)/admin/logs/page.tsx
new file mode 100644
index 0000000..615d690
--- /dev/null
+++ b/src/app/(admin)/admin/logs/page.tsx
@@ -0,0 +1,152 @@
+"use client";
+
+import { redirect } from "next/navigation";
+import type { ExtendedAccessToken } from "@/types";
+import { Search } from "lucide-react";
+
+import { prisma } from "@/lib/prisma";
+import { getCurrentUser } from "@/lib/session";
+import { Badge } from "@/components/ui/badge";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+async function getLogs(search?: string) {
+ const thirtyDaysAgo = new Date();
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
+
+ const logs = await prisma.accessToken.findMany({
+ where: {
+ expiresAt: {
+ gte: thirtyDaysAgo,
+ },
+ ...(search
+ ? {
+ OR: [{ clientId: search }, { userId: search }],
+ }
+ : {}),
+ },
+ include: {
+ client: {
+ select: {
+ name: true,
+ },
+ },
+ user: {
+ select: {
+ username: true,
+ },
+ },
+ },
+ orderBy: [
+ {
+ expiresAt: "desc",
+ },
+ ],
+ take: 100,
+ });
+
+ return logs as ExtendedAccessToken[];
+}
+
+export default async function LogsPage({
+ searchParams,
+}: {
+ searchParams: { search?: string };
+}) {
+ const user = await getCurrentUser();
+ if (!user || user.role !== "ADMIN") {
+ redirect("/dashboard");
+ }
+
+ const logs = await getLogs(searchParams.search);
+
+ return (
+
+
+
+
+
+ 系统日志
+
+ 显示最近30天的授权记录和错误日志
+
+
+
+
+
+
+
+
+
+
+
+
+ 时间
+ 应用
+ 用户
+ 令牌
+ 状态
+ 错误信息
+
+
+
+ {logs.map((log: ExtendedAccessToken) => (
+
+
+ {new Date(log.createdAt).toLocaleString()}
+
+ {log.client?.name || "-"}
+ {log.user?.username || "-"}
+
+ {log.id.slice(0, 8)}...
+
+
+ new Date()
+ ? "default"
+ : "secondary"
+ }
+ >
+ {log.error
+ ? "错误"
+ : new Date(log.expiresAt) > new Date()
+ ? "有效"
+ : "过期"}
+
+
+
+ {log.error || "-"}
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/app/(admin)/admin/page.tsx b/src/app/(admin)/admin/page.tsx
index 5817558..0ccb823 100644
--- a/src/app/(admin)/admin/page.tsx
+++ b/src/app/(admin)/admin/page.tsx
@@ -1,7 +1,10 @@
+import Link from "next/link";
import { redirect } from "next/navigation";
+import { Activity, AppWindow, FileText, Users } from "lucide-react";
import { prisma } from "@/lib/prisma";
import { getCurrentUser } from "@/lib/session";
+import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -9,22 +12,29 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
async function getStats() {
- const [userCount, clientCount] = await Promise.all([
- prisma.user.count(),
- prisma.client.count(),
- ]);
+ const [userCount, clientCount, activeTokenCount, recentAuthCount] =
+ await Promise.all([
+ prisma.user.count(),
+ prisma.client.count(),
+ prisma.accessToken.count({
+ where: {
+ expiresAt: {
+ gt: new Date(),
+ },
+ },
+ }),
+ prisma.authorization.count({
+ where: {
+ createdAt: {
+ gt: new Date(Date.now() - 24 * 60 * 60 * 1000), // 最近24小时
+ },
+ },
+ }),
+ ]);
- const clients = await prisma.client.findMany({
+ const recentClients = await prisma.client.findMany({
include: {
user: {
select: {
@@ -36,13 +46,15 @@ async function getStats() {
orderBy: {
createdAt: "desc",
},
- take: 10,
+ take: 5,
});
return {
userCount,
clientCount,
- recentClients: clients,
+ activeTokenCount,
+ recentAuthCount,
+ recentClients,
};
}
@@ -56,58 +68,145 @@ export default async function AdminPage() {
return (
-
+
- 用户统计
- 系统中的总用户数
+
+
+ 用户管理
+
+ 管理系统中的用户账号
-
+
{stats.userCount}
+
+
+
- 应用统计
- 系统中的总应用数
+
+
+ 应用管理
+
+ 管理系统中的应用
-
+
{stats.clientCount}
+
+
+
+
+
+
+
+
+
+
+ 系统状态
+
+ 查看系统运行状态
+
+
+ {stats.activeTokenCount}
+ 活跃访问令牌数
+
+
+
+
+
+
+
+ 系统日志
+
+ 查看系统日志记录
+
+
+ {stats.recentAuthCount}
+
+
+ 24小时内新增授权数
+
+
+
+
+
-
-
- 最近创建的应用
- 显示最近创建的 10 个应用
-
-
-
-
-
- 应用名称
- 创建者
- 创建时间
- Client ID
-
-
-
+
+
+
+ 最近创建的应用
+ 显示最近创建的 5 个应用
+
+
+
{stats.recentClients.map((client) => (
-
- {client.name}
- {client.user.username}
-
- {new Date(client.createdAt).toLocaleString()}
-
- {client.clientId}
-
+
+
+
{client.name}
+
+ 由 {client.user.username} 创建于{" "}
+ {new Date(client.createdAt).toLocaleString()}
+
+
+
+
+
+
))}
-
-
-
-
+
+
+
+
+
+
+ 系统信息
+ 显示系统运行状态信息
+
+
+
+
+
Node.js 版本
+
+ {process.version}
+
+
+
+
系统平台
+
+ {process.platform}
+
+
+
+
系统架构
+
{process.arch}
+
+
+
+
运行时长
+
+ {Math.floor(process.uptime())} 秒
+
+
+
+
+
+
);
}
diff --git a/src/app/(admin)/admin/users/page.tsx b/src/app/(admin)/admin/users/page.tsx
new file mode 100644
index 0000000..78c3b26
--- /dev/null
+++ b/src/app/(admin)/admin/users/page.tsx
@@ -0,0 +1,117 @@
+import { redirect } from "next/navigation";
+import { Search } from "lucide-react";
+
+import { prisma } from "@/lib/prisma";
+import { getCurrentUser } from "@/lib/session";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+async function getUsers(search?: string) {
+ const where = search
+ ? {
+ OR: [
+ { username: { contains: search } },
+ { email: { contains: search } },
+ { name: { contains: search } },
+ ],
+ }
+ : {};
+
+ const users = await prisma.user.findMany({
+ where,
+ orderBy: {
+ createdAt: "desc",
+ },
+ });
+
+ return users;
+}
+
+export default async function UsersPage({
+ searchParams,
+}: {
+ searchParams: { search?: string };
+}) {
+ const user = await getCurrentUser();
+ if (!user || user.role !== "ADMIN") {
+ redirect("/dashboard");
+ }
+
+ const users = await getUsers(searchParams.search);
+
+ return (
+
+
+
+
+
+ 用户管理
+ 查看和管理系统中的所有用户
+
+
+
+
+
+
+
+
+
+
+
+ 用户名
+ 邮箱
+ 昵称
+ 角色
+ 创建时间
+ 用户组
+
+
+
+ {users.map((user) => (
+
+ {user.username}
+ {user.email}
+ {user.name || "-"}
+
+ {user.role === "ADMIN"
+ ? "管理员"
+ : user.moderator
+ ? "版主"
+ : "用户"}
+
+
+ {new Date(user.createdAt).toLocaleString()}
+
+
+ {user.groups?.length ? user.groups.join(", ") : "-"}
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
new file mode 100644
index 0000000..d3d5d60
--- /dev/null
+++ b/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
new file mode 100644
index 0000000..af6a9ee
--- /dev/null
+++ b/src/types/index.d.ts
@@ -0,0 +1,33 @@
+///
+///
+///
+
+import {
+ AccessToken as PrismaAccessToken,
+ Client as PrismaClient,
+ User,
+} from "@prisma/client";
+
+export interface ExtendedClient extends PrismaClient {
+ user: {
+ username: string;
+ email: string;
+ };
+ accessTokens?: ExtendedAccessToken[];
+ authCount?: number;
+ enabled?: boolean;
+}
+
+export interface ExtendedAccessToken extends PrismaAccessToken {
+ client?: {
+ name: string;
+ };
+ user?: {
+ username: string;
+ };
+ error?: string | null;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+export {};