mirror of
https://github.com/woodchen-ink/Q58Connect.git
synced 2025-07-18 14:01:55 +08:00
feat: Enhance authorization UI and add analytics tracking
This commit is contained in:
parent
8be59fe6b6
commit
a3fe3ec419
@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { getClientsByUserId } from "@/lib/dto/client";
|
import { getClientsByUserId } from "@/lib/dto/client";
|
||||||
import { getCurrentUser } from "@/lib/session";
|
import { getCurrentUser } from "@/lib/session";
|
||||||
@ -15,6 +16,8 @@ import {
|
|||||||
import { AddClientButton } from "@/components/clients/add-client";
|
import { AddClientButton } from "@/components/clients/add-client";
|
||||||
import { DeleteClientButton } from "@/components/clients/delete-client";
|
import { DeleteClientButton } from "@/components/clients/delete-client";
|
||||||
|
|
||||||
|
type Client = Awaited<ReturnType<typeof getClientsByUserId>>[number];
|
||||||
|
|
||||||
// 创建 Prisma 客户端实例
|
// 创建 Prisma 客户端实例
|
||||||
async function fetchClients(userId: string) {
|
async function fetchClients(userId: string) {
|
||||||
return await getClientsByUserId(userId);
|
return await getClientsByUserId(userId);
|
||||||
@ -50,7 +53,7 @@ export default async function ClientsPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{clients.map((client) => (
|
{clients.map((client: Client) => (
|
||||||
<TableRow key={client.id}>
|
<TableRow key={client.id}>
|
||||||
<TableCell className="font-medium">{client.name}</TableCell>
|
<TableCell className="font-medium">{client.name}</TableCell>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell className="font-mono text-sm">
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
|
||||||
import { Providers } from "./providers";
|
import { Providers } from "./providers";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Q58 Connect",
|
title: "Q58 Connect",
|
||||||
description: "Q58 Connect, 基于Q58论坛的OAuth 2.0认证服务",
|
description: "Q58论坛 OAuth 认证服务",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -18,9 +21,20 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="zh-CN" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<Script
|
||||||
|
defer
|
||||||
|
src="https://analytics.czl.net/script.js"
|
||||||
|
data-website-id="78784a68-cb2f-42ae-851b-89c034efd05e"
|
||||||
|
strategy="afterInteractive"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<Providers>{children}</Providers>
|
<Providers>
|
||||||
|
{children}
|
||||||
|
<Toaster />
|
||||||
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { handleAuthorizeAction } from "@/actions/authorizing";
|
import { handleAuthorizeAction } from "@/actions/authorizing";
|
||||||
import { Client } from "@prisma/client";
|
|
||||||
import {
|
import {
|
||||||
ChevronsDownUp,
|
ChevronsDownUp,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
@ -21,6 +20,8 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
||||||
|
import { Client } from ".prisma/client";
|
||||||
|
|
||||||
interface Permission {
|
interface Permission {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -45,6 +46,7 @@ export function AuthorizationCard({
|
|||||||
const [expandedPermission, setExpandedPermission] = useState<string | null>(
|
const [expandedPermission, setExpandedPermission] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [isAuthorizing, setIsAuthorizing] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const togglePermission = (id: string) => {
|
const togglePermission = (id: string) => {
|
||||||
@ -52,75 +54,99 @@ export function AuthorizationCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const authorizingHandler = async () => {
|
const authorizingHandler = async () => {
|
||||||
const url = await handleAuthorizeAction(
|
try {
|
||||||
oauthParams,
|
setIsAuthorizing(true);
|
||||||
client.userId,
|
const url = await handleAuthorizeAction(
|
||||||
client.id,
|
oauthParams,
|
||||||
permissions[0].id,
|
client.userId,
|
||||||
);
|
client.id,
|
||||||
router.push(url);
|
permissions[0].id,
|
||||||
|
);
|
||||||
|
router.push(url);
|
||||||
|
} catch (error) {
|
||||||
|
setIsAuthorizing(false);
|
||||||
|
// 这里可以添加错误提示
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-2xl">
|
<Card className="w-full max-w-2xl transform transition-all duration-300 hover:shadow-lg">
|
||||||
<CardHeader className="text-center">
|
<CardHeader className="space-y-4 text-center">
|
||||||
<div className="mb-4 flex items-center justify-center space-x-4">
|
<div className="flex items-center justify-center space-x-6">
|
||||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
|
<div className="group relative">
|
||||||
<span className="text-3xl font-bold text-red-500">B</span>
|
<div className="absolute -inset-0.5 rounded-full bg-gradient-to-r from-pink-600 to-purple-600 opacity-50 blur transition duration-300 group-hover:opacity-75"></div>
|
||||||
|
<div className="relative flex h-20 w-20 items-center justify-center rounded-full bg-white">
|
||||||
|
<span className="bg-gradient-to-r from-pink-600 to-purple-600 bg-clip-text text-4xl font-bold text-transparent">
|
||||||
|
{client.name[0].toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GithubIcon className="h-16 w-16" />
|
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl font-bold">授权 {client.name}</CardTitle>
|
<CardTitle className="bg-gradient-to-r from-pink-600 to-purple-600 bg-clip-text text-3xl font-bold text-transparent">
|
||||||
</CardHeader>
|
授权 {client.name}
|
||||||
<CardContent>
|
</CardTitle>
|
||||||
<p className="mb-6 text-center">
|
<p className="text-sm text-gray-500">
|
||||||
{client.description}
|
该应用程序请求访问您的Q58论坛账号
|
||||||
<br />
|
|
||||||
想要访问您的 Q58论坛 账户
|
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-4">
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-3 rounded-lg bg-gray-50 p-4">
|
||||||
|
<h3 className="text-lg font-semibold">请求的权限</h3>
|
||||||
{permissions.map((permission) => (
|
{permissions.map((permission) => (
|
||||||
<div key={permission.id} className="rounded-lg border p-4">
|
<div
|
||||||
<div
|
key={permission.id}
|
||||||
className="flex cursor-pointer items-center justify-between"
|
className="rounded-md border bg-white p-4 transition-all duration-200 hover:border-purple-200"
|
||||||
onClick={() => togglePermission(permission.id)}
|
onClick={() => togglePermission(permission.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex cursor-pointer items-center justify-between">
|
||||||
<Checkbox
|
<div className="flex items-center space-x-2">
|
||||||
id={permission.id}
|
<Checkbox id={permission.id} checked disabled />
|
||||||
checked={permission.id === "read_profile"}
|
|
||||||
disabled={permission.id === "read_profile"}
|
|
||||||
/>
|
|
||||||
<label htmlFor={permission.id} className="font-medium">
|
<label htmlFor={permission.id} className="font-medium">
|
||||||
{permission.name}
|
{permission.name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{expandedPermission === permission.id ? (
|
{expandedPermission === permission.id ? (
|
||||||
<ChevronsDownUp />
|
<ChevronsDownUp className="h-4 w-4 text-gray-500" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronsUpDown />
|
<ChevronsUpDown className="h-4 w-4 text-gray-500" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{expandedPermission === permission.id && (
|
{expandedPermission === permission.id && (
|
||||||
<p className="mt-2 text-sm text-gray-600">
|
<div className="mt-2 pl-6 text-sm text-gray-500">
|
||||||
{permission.description}
|
{permission.description}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col items-center">
|
|
||||||
<div className="mb-4 flex space-x-4">
|
<CardFooter className="flex flex-col items-center space-y-4">
|
||||||
<Button variant="outline">取消</Button>
|
<div className="flex space-x-4">
|
||||||
<Button
|
<Button
|
||||||
className="bg-green-600 text-white hover:bg-green-700"
|
variant="outline"
|
||||||
onClick={authorizingHandler}
|
className="min-w-[100px] transition-all duration-200 hover:bg-gray-100"
|
||||||
|
disabled={isAuthorizing}
|
||||||
>
|
>
|
||||||
授权 {client.name}
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="min-w-[100px] bg-gradient-to-r from-pink-600 to-purple-600 text-white transition-all duration-200 hover:from-pink-700 hover:to-purple-700"
|
||||||
|
onClick={authorizingHandler}
|
||||||
|
disabled={isAuthorizing}
|
||||||
|
>
|
||||||
|
{isAuthorizing ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"></div>
|
||||||
|
授权中...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
`授权 ${client.name}`
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-4 text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
授权将重定向到 {client.redirectUri}
|
授权将重定向到 {client.redirectUri}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center space-x-8 text-sm text-gray-500">
|
<div className="flex justify-center space-x-8 text-sm text-gray-500">
|
||||||
|
@ -8,6 +8,7 @@ export function Authorizing() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [error, setError] = useState<unknown | null>(null);
|
const [error, setError] = useState<unknown | null>(null);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
const signInCallback = useCallback(async () => {
|
const signInCallback = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -19,20 +20,64 @@ export function Authorizing() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 模拟进度条
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
setProgress((prev) => (prev >= 90 ? 90 : prev + 10));
|
||||||
|
}, 300);
|
||||||
|
|
||||||
// Delay 3s get sso url go to ...
|
// Delay 3s get sso url go to ...
|
||||||
const timer = setTimeout(signInCallback, 3);
|
const timer = setTimeout(signInCallback, 3000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
clearInterval(progressInterval);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
||||||
{error ? (
|
{error ? (
|
||||||
<p className="text-center">授权异常,登录失败!</p>
|
<div className="space-y-4 text-center">
|
||||||
|
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
|
||||||
|
<svg
|
||||||
|
className="h-8 w-8 text-red-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900">授权异常</h3>
|
||||||
|
<p className="text-gray-500">登录失败,请稍后重试!</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="mt-4 rounded-md bg-red-600 px-4 py-2 text-white transition-colors hover:bg-red-700"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center"> 获取授权信息,等待跳转中,请稍等...</p>
|
<div className="space-y-4 text-center">
|
||||||
|
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-blue-100">
|
||||||
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900">正在授权</h3>
|
||||||
|
<p className="text-gray-500">获取授权信息中,请稍等...</p>
|
||||||
|
<div className="h-2.5 w-full rounded-full bg-gray-200">
|
||||||
|
<div
|
||||||
|
className="h-2.5 rounded-full bg-blue-600 transition-all duration-300 ease-out"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-400">系统正在处理您的请求</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Client } from "@prisma/client";
|
|
||||||
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -16,6 +15,8 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
import type { Client } from ".prisma/client";
|
||||||
|
|
||||||
interface EditClientFormProps {
|
interface EditClientFormProps {
|
||||||
client: Client;
|
client: Client;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user