refactor: Simplify OAuth authorization page and component

- Restructure AuthorizePage to handle user authentication and client validation
- Modify Authorizing component to use new authorization action
- Improve error handling with dedicated ErrorCard component
- Streamline authorization flow and parameter handling
- Remove redundant validation checks in favor of centralized authorization logic
This commit is contained in:
wood chen 2025-02-20 03:09:46 +08:00
parent 493ad7136f
commit a82643ada4
3 changed files with 121 additions and 93 deletions

View File

@ -1,13 +1,14 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getClientByClientId } from "@/lib/dto/client"; import { getClientByClientId } from "@/lib/dto/client";
import { getCurrentUser } from "@/lib/session";
import { Authorizing } from "@/components/auth/authorizing"; import { Authorizing } from "@/components/auth/authorizing";
export interface AuthorizeParams extends Record<string, string> { export interface AuthorizeParams {
oauth: string;
clientId: string;
scope: string; scope: string;
response_type: string; redirectUri: string;
client_id: string;
redirect_uri: string;
} }
export default async function AuthorizePage({ export default async function AuthorizePage({
@ -15,51 +16,23 @@ export default async function AuthorizePage({
}: { }: {
searchParams: AuthorizeParams; searchParams: AuthorizeParams;
}) { }) {
// 检查必要的参数 const user = await getCurrentUser();
if ( if (!user?.id) {
!searchParams.response_type || redirect("/login");
!searchParams.client_id ||
!searchParams.redirect_uri
) {
const errorParams = new URLSearchParams({
error: "invalid_request",
error_description: "缺少必要的参数",
});
redirect(`${searchParams.redirect_uri}?${errorParams.toString()}`);
} }
const client = await getClientByClientId(searchParams.client_id); const client = await getClientByClientId(searchParams.clientId);
// 检查应用是否存在
if (!client) { if (!client) {
const errorParams = new URLSearchParams({ return (
error: "invalid_client", <div className="flex min-h-screen items-center justify-center p-4">
error_description: "应用不存在", <div className="text-center text-red-600"></div>
}); </div>
redirect(`${searchParams.redirect_uri}?${errorParams.toString()}`); );
}
// 检查回调地址是否匹配
if (client.redirectUri !== searchParams.redirect_uri) {
const errorParams = new URLSearchParams({
error: "invalid_request",
error_description: "回调地址不匹配",
});
redirect(`${searchParams.redirect_uri}?${errorParams.toString()}`);
}
// 检查应用是否被禁用
if (!client.enabled) {
const errorParams = new URLSearchParams({
error: "access_denied",
error_description: "此应用已被禁用",
});
redirect(`${searchParams.redirect_uri}?${errorParams.toString()}`);
} }
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4"> <div className="flex min-h-screen items-center justify-center p-4">
<Authorizing searchParams={searchParams} /> <Authorizing {...searchParams} />
</div> </div>
); );
} }

View File

@ -2,74 +2,61 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { getDiscourseSSOUrl } from "@/actions/discourse-sso-url"; import { handleAuthorizeAction } from "@/actions/authorizing";
import type { AuthorizeParams } from "@/app/(oauth)/oauth/authorize/page"; import { ErrorCard } from "@/components/auth/error-card";
interface AuthorizingProps { interface AuthorizingProps {
searchParams: AuthorizeParams; oauth: string;
clientId: string;
scope: string;
redirectUri: string;
} }
export function Authorizing({ searchParams }: AuthorizingProps) { export function Authorizing({
const router = useRouter(); oauth,
const [error, setError] = useState<unknown | null>(null); clientId,
scope,
redirectUri,
}: AuthorizingProps) {
const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
// 立即开始授权 const authorize = async () => {
const doAuth = async () => { const result = await handleAuthorizeAction(oauth, clientId, scope);
try { if (result.error) {
const url = await getDiscourseSSOUrl( setError(result.error);
new URLSearchParams(searchParams).toString(), } else if (result.redirectUrl) {
); const url = await result.redirectUrl;
router.push(url); window.location.href = url;
} catch (error) {
setError(error);
} }
}; };
doAuth(); authorize().catch((err) => {
}, [searchParams, router]); console.error("授权过程出错:", err);
setError("授权过程发生错误,请稍后重试");
});
}, [oauth, clientId, scope]);
if (error) { if (error) {
return ( return (
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl"> <div className="flex min-h-screen items-center justify-center p-4">
<div className="space-y-4 text-center"> <ErrorCard
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-100"> title="授权失败"
<svg description={error}
className="h-8 w-8 text-red-600" redirectUri={redirectUri}
fill="none" error="access_denied"
stroke="currentColor" errorDescription={error}
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>
</div> </div>
); );
} }
return ( return (
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl"> <div className="flex min-h-screen items-center justify-center">
<div className="space-y-4 text-center"> <div className="text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-blue-100"> <div className="mb-4 text-2xl font-semibold">...</div>
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div> <div className="text-gray-500"></div>
</div>
<h3 className="text-xl font-semibold text-gray-900"></h3>
<p className="text-gray-500">...</p>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,68 @@
import { useRouter } from "next/navigation";
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
interface ErrorCardProps {
title: string;
description: string;
redirectUri?: string;
error?: string;
errorDescription?: string;
}
export function ErrorCard({
title,
description,
redirectUri,
error,
errorDescription,
}: ErrorCardProps) {
const router = useRouter();
const handleBack = () => {
if (redirectUri) {
const url = new URL(redirectUri);
if (error) url.searchParams.set("error", error);
if (errorDescription)
url.searchParams.set("error_description", errorDescription);
router.push(url.toString());
} else {
router.push("/");
}
};
return (
<Card className="w-full max-w-md">
<CardHeader className="space-y-4 text-center">
<div className="flex items-center justify-center">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-red-50">
<AlertCircle className="h-10 w-10 text-red-600" />
</div>
</div>
<CardTitle className="text-2xl font-bold text-red-600">
{title}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-gray-500">{description}</p>
</CardContent>
<CardFooter className="flex justify-center">
<Button
variant="outline"
className="min-w-[120px] border-red-200 text-red-600 hover:bg-red-50 hover:text-red-700"
onClick={handleBack}
>
</Button>
</CardFooter>
</Card>
);
}