224 lines
7.0 KiB
TypeScript
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>
|
|
);
|
|
}
|