gh_UserManager/apps/web/app/oauth/consent/page.tsx
bermooda-company 54d5891edf user
2026-08-23 23:59:14 +03:30

224 lines
7.0 KiB
TypeScript

"use client";
import { useSearchParams, useRouter } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { LoginForm } from "@/components/auth/login-form";
import { useLanguage } from "@/components/language-provider";
interface Application {
client_id: string;
name: string;
redirect_uris: string[];
}
interface ConsentData {
application: Application;
scopes: string[];
login_url: string;
}
function ConsentContent() {
const searchParams = useSearchParams();
const router = useRouter();
const { lang, dir } = useLanguage();
const client_id = searchParams.get("client_id") || "";
const redirect_uri = searchParams.get("redirect_uri") || "";
const scope = searchParams.get("scope") || "openid";
const state = searchParams.get("state") || "";
const nonce = searchParams.get("nonce") || "";
const code_challenge = searchParams.get("code_challenge") || "";
const code_challenge_method = searchParams.get("code_challenge_method") || "plain";
const [consentData, setConsentData] = useState<ConsentData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [token, setToken] = useState<string | null>(null);
const t = (fa: string, en: string) => (lang === "fa" ? fa : en);
const fetchConsent = async () => {
try {
const storedToken = localStorage.getItem("access_token");
if (storedToken) {
setToken(storedToken);
}
const params = new URLSearchParams({
client_id,
redirect_uri,
response_type: "code",
scope,
state,
nonce,
code_challenge,
code_challenge_method,
});
const headers: Record<string, string> = {};
if (storedToken) {
headers["Authorization"] = `Bearer ${storedToken}`;
}
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/oauth/authorize?${params.toString()}`,
{ headers }
);
if (res.status === 401) {
const data = await res.json();
setConsentData({
application: data.application || { client_id, name: "", redirect_uris: [redirect_uri] },
scopes: data.scopes || scope.split(" "),
login_url: data.login_url,
});
setToken(null);
} else if (res.ok) {
const data = await res.json();
setConsentData({
application: data.application || { client_id, name: "", redirect_uris: [redirect_uri] },
scopes: data.scopes || scope.split(" "),
login_url: "",
});
}
} catch (e) {
setError("خطا در بارگذاری اطلاعات برنامه");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchConsent();
}, []);
const handleConsent = async () => {
if (!token) return;
const params = new URLSearchParams({
client_id,
redirect_uri,
response_type: "code",
scope,
state,
nonce,
code_challenge,
code_challenge_method,
});
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/oauth/authorize?${params.toString()}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
if (res.ok) {
const data = await res.json();
const code = data.code;
const redirectParams = new URLSearchParams();
redirectParams.set("code", code);
if (state) redirectParams.set("state", state);
window.location.href = `${redirect_uri}?${redirectParams.toString()}`;
}
};
const handleLogin = (access: string) => {
setToken(access);
handleConsent();
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-ink">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-400 mx-auto"></div>
<p className="mt-4 text-slate-400">{t("در حال بارگذاری...", "Loading...")}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-ink flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="rounded-xl border border-white/10 bg-white/[0.02] p-6">
<h2 className="text-xl font-bold text-white mb-4">
{t("تایید دسترسی", "Authorize Application")}
</h2>
{error && (
<div className="mb-4 text-sm text-rose-400">{error}</div>
)}
{consentData && (
<div className="space-y-4">
<div>
<p className="text-sm text-slate-400">
{t("برنامه درخواست دسترسی به:", "This application requests access to:")}
</p>
<div className="mt-2 flex items-center gap-3">
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-brand-gradient text-xs font-bold text-white">
{consentData.application?.name?.charAt(0) || "App"}
</span>
<div>
<div className="font-medium text-white">
{consentData.application?.name || client_id}
</div>
<div className="text-xs text-slate-500">{client_id}</div>
</div>
</div>
</div>
<div>
<p className="text-sm text-slate-400">
{t("دسترسی‌های درخواستی:", "Requested permissions:")}
</p>
<ul className="mt-2 space-y-1">
{consentData.scopes.map((s: string) => (
<li key={s} className="text-sm text-slate-300">
{s}
</li>
))}
</ul>
</div>
{!token ? (
<LoginForm
onSuccess={handleLogin}
redirectUri={redirect_uri}
state={state}
/>
) : (
<div className="flex gap-3">
<button
onClick={() => window.location.href = "/"}
className="flex-1 rounded-lg border border-white/10 px-4 py-2 text-sm text-slate-300 hover:bg-white/5"
>
{t("لغو", "Cancel")}
</button>
<button
onClick={handleConsent}
className="flex-1 rounded-lg bg-brand-gradient px-4 py-2 text-sm font-medium text-white hover:brightness-110"
>
{t("تایید", "Authorize")}
</button>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}
export default function ConsentPage() {
return (
<Suspense fallback={<div className="flex items-center justify-center min-h-screen bg-ink"><div className="text-slate-400">Loading...</div></div>}>
<ConsentContent />
</Suspense>
);
}