/** * مدیریت SSO — ارتباط با Identity Service و Keycloak. * PKCE کاملاً سمت سرور (Identity Service) مدیریت می‌شود تا به origin/مرورگر * وابسته نباشد. توکن‌ها فقط در sessionStorage نگهداری می‌شوند. */ const IDENTITY_API = process.env.NEXT_PUBLIC_IDENTITY_API_URL || "http://localhost:8001"; const TOKEN_KEY = "superapp_access_token"; const REFRESH_KEY = "superapp_refresh_token"; export interface AuthConfig { issuer: string; client_id: string; authorization_endpoint: string; token_endpoint: string; logout_endpoint: string; callback_url: string; } export interface LoginUrlResponse { authorization_url: string; state: string; } export interface TokenBundle { access_token: string; refresh_token?: string; expires_in: number; token_type: string; } export function getStoredToken(): string | null { if (typeof window === "undefined") return null; return sessionStorage.getItem(TOKEN_KEY); } export function storeTokens(tokens: TokenBundle): void { sessionStorage.setItem(TOKEN_KEY, tokens.access_token); if (tokens.refresh_token) { sessionStorage.setItem(REFRESH_KEY, tokens.refresh_token); } } export function clearTokens(): void { sessionStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(REFRESH_KEY); } export async function fetchAuthConfig(): Promise { let res: Response; try { res = await fetch(`${IDENTITY_API}/api/v1/auth/config`); } catch { throw new Error( `اتصال به سرویس احراز هویت برقرار نشد (${IDENTITY_API}). مطمئن شوید identity-access-service در حال اجراست.` ); } if (!res.ok) throw new Error("دریافت پیکربندی SSO ناموفق بود"); return res.json(); } const POST_LOGIN_REDIRECT_KEY = "superapp_post_login_redirect"; export function setPostLoginRedirect(path: string): void { if (typeof window === "undefined") return; sessionStorage.setItem(POST_LOGIN_REDIRECT_KEY, path); } export function consumePostLoginRedirect(fallback = "/dashboard"): string { if (typeof window === "undefined") return fallback; const path = sessionStorage.getItem(POST_LOGIN_REDIRECT_KEY) || fallback; sessionStorage.removeItem(POST_LOGIN_REDIRECT_KEY); return path; } export async function loginWithRedirect(redirectPath?: string): Promise { if (redirectPath) setPostLoginRedirect(redirectPath); const qs = redirectPath ? `?redirect=${encodeURIComponent(redirectPath)}` : ""; window.location.href = `/login${qs}`; } /** آدرس ورود (شامل state و PKCE challenge) را از سرور می‌گیرد. */ export async function fetchLoginUrl(): Promise { let res: Response; try { res = await fetch(`${IDENTITY_API}/api/v1/auth/login-url`); } catch { throw new Error( `اتصال به سرویس احراز هویت برقرار نشد (${IDENTITY_API}). مطمئن شوید identity-access-service در حال اجراست.` ); } if (!res.ok) throw new Error("ساخت آدرس ورود ناموفق بود"); const data: LoginUrlResponse = await res.json(); return data.authorization_url; } export async function redeemHandoff(handoffCode: string): Promise { const res = await fetch(`${IDENTITY_API}/api/v1/auth/session/redeem`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ handoff_code: handoffCode }), }); if (!res.ok) { let detail = ""; try { const body = await res.json(); detail = body?.error?.message || body?.detail || ""; } catch { /* ignore */ } throw new Error(detail || "تبدیل session ناموفق بود"); } return res.json(); } export async function exchangeCode( code: string, redirectUri: string, state: string | null ): Promise { const res = await fetch(`${IDENTITY_API}/api/v1/auth/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code, redirect_uri: redirectUri, state, }), }); if (!res.ok) { let detail = ""; try { const body = await res.json(); detail = body?.error?.message || body?.detail || ""; } catch { /* ignore */ } throw new Error( detail ? `تبدیل code به token ناموفق بود: ${detail}` : "تبدیل code به token ناموفق بود" ); } return res.json(); } export async function fetchMe(token: string) { const res = await fetch(`${IDENTITY_API}/api/v1/auth/me`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error("دریافت اطلاعات کاربر ناموفق بود"); return res.json() as Promise<{ user_id: string; email: string | null; username: string | null; mobile: string | null; mobile_verified: boolean; requires_mobile_verification: boolean; roles: string[]; }>; } export async function logout(config: AuthConfig): Promise { clearTokens(); const params = new URLSearchParams({ client_id: config.client_id, post_logout_redirect_uri: window.location.origin, }); window.location.href = `${config.logout_endpoint}?${params.toString()}`; }