/** * API Client — ارتباط frontend با Core Backend (پنل ادمین / tenant). * توکن SSO مرکزی (Keycloak) از sessionStorage استفاده می‌شود. */ import { getValidAccessToken } from "@/lib/auth"; const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000"; export class ApiError extends Error { constructor( public status: number, public code: string, message: string ) { super(message); this.name = "ApiError"; } } interface RequestOptions extends RequestInit { auth?: boolean; } async function request( path: string, options: RequestOptions = {} ): Promise { const { auth = true, headers: customHeaders, ...init } = options; const headers = new Headers(customHeaders); headers.set("Content-Type", "application/json"); if (auth) { const token = await getValidAccessToken(); if (token) headers.set("Authorization", `Bearer ${token}`); } let res: Response; try { res = await fetch(`${API_BASE}${path}`, { ...init, headers }); } catch { throw new Error(`اتصال به سرور برقرار نشد (${API_BASE})`); } if (res.status === 401 && auth) { const { refreshSession } = await import("@/lib/auth"); const refreshed = await refreshSession({ force: true }); if (refreshed) { headers.set("Authorization", `Bearer ${refreshed}`); res = await fetch(`${API_BASE}${path}`, { ...init, headers }); } } if (!res.ok) { const body = await res.json().catch(() => ({})); throw new ApiError( res.status, body?.error?.code || "unknown_error", body?.error?.message || res.statusText ); } if (res.status === 204) return undefined as T; return res.json() as Promise; } export interface PublicTenantSite { tenant_id: string; name: string; slug: string; status: string; business_type: string | null; primary_color: string | null; secondary_color: string | null; logo_url: string | null; favicon_url: string | null; primary_domain: string | null; tagline: string; } export interface OtpRequestResult { message: string; expires_in: number; skipped?: boolean; } type OtpContext = "public" | "admin"; export interface OtpVerifyResult { access_token: string; refresh_token: string; token_type: string; expires_in: number; is_new_user: boolean; user_id: string; role: string; } export interface Tenant { id: string; name: string; slug: string; status: string; owner_user_id: string | null; business_type?: string | null; default_locale?: string; timezone?: string; primary_color?: string | null; secondary_color?: string | null; logo_url?: string | null; favicon_url?: string | null; onboarding_completed?: boolean; created_at: string; updated_at: string; } export interface Page { items: T[]; meta: { page: number; page_size: number; total_items: number; total_pages: number; }; } // ---- فاز ۳: Onboarding و Tenant Context ---- export interface MembershipSummary { tenant_id: string; tenant_name: string; tenant_slug: string; tenant_status: string; onboarding_completed: boolean; role: string; status: string; is_owner: boolean; } export interface MeResponse { user_id: string; mobile: string; email: string | null; platform_role: string; onboarding_required: boolean; current_tenant_id: string | null; memberships: MembershipSummary[]; } export interface TenantDomain { id: string; tenant_id: string; domain: string; domain_type: string; is_verified: boolean; verified_at: string | null; created_at: string; } export interface TenantContext { tenant: Tenant; role: string | null; is_owner: boolean; plan_code: string | null; plan_name: string | null; subscription_status: string | null; domains: TenantDomain[]; primary_domain: string | null; } export interface OnboardingTenantCreateInput { business_name: string; slug: string; business_type?: string; default_locale?: string; timezone?: string; } export interface OnboardingBrandingInput { primary_color?: string; secondary_color?: string; logo_url?: string; favicon_url?: string; } export interface OnboardingDomainInput { custom_domain?: string; } // ---- مدیریت پلتفرم (پنل ادمین): پلن، قابلیت، اشتراک، سرویس ---- export interface Plan { id: string; code: string; name: string; description: string | null; is_active: boolean; created_at: string; } export interface Feature { id: string; feature_key: string; name: string; description: string | null; service_key: string; is_active: boolean; } export type LimitPeriod = "day" | "month" | "year" | "total"; export interface PlanFeature { id: string; plan_id: string; feature_id: string; limit_value: number | null; limit_period: LimitPeriod | null; is_enabled: boolean; } export type SubscriptionStatus = | "trialing" | "active" | "past_due" | "canceled" | "expired"; export interface Subscription { id: string; tenant_id: string; plan_id: string; status: SubscriptionStatus; starts_at: string | null; ends_at: string | null; trial_ends_at: string | null; created_at: string; } export type ServiceStatus = "active" | "inactive" | "maintenance"; export interface ServiceEntry { id: string; service_key: string; name: string; base_url: string | null; status: ServiceStatus; health_check_url: string | null; created_at: string; } export interface FeatureCheckResult { tenant_id: string; feature_key: string; has_access: boolean; reason: string | null; } export const api = { public: { tenantSite: (opts?: { slug?: string | null; host?: string | null }) => { const params = new URLSearchParams(); if (opts?.slug) params.set("slug", opts.slug); const q = params.toString(); const headers: Record = {}; if (opts?.slug) headers["X-Tenant-Slug"] = opts.slug; if (opts?.host) headers["X-Forwarded-Host"] = opts.host; return request( `/api/v1/public/tenant-site${q ? `?${q}` : ""}`, { auth: false, headers } ); }, }, auth: { requestOtp: (mobile: string, options?: { forceSms?: boolean; context?: OtpContext }) => request("/api/v1/auth/otp/request", { method: "POST", auth: false, body: JSON.stringify({ mobile, context: options?.context ?? "admin", ...(options?.forceSms ? { force_sms: true } : {}), }), }), verifyOtp: (mobile: string, code: string, context: OtpContext = "admin") => request("/api/v1/auth/otp/verify", { method: "POST", auth: false, body: JSON.stringify({ mobile, code, context }), }), }, tenants: { list: (page = 1, pageSize = 20) => request>( `/api/v1/admin/tenants?page=${page}&page_size=${pageSize}` ), get: (id: string) => request(`/api/v1/admin/tenants/${id}`), create: (data: { name: string; slug: string; custom_domain?: string }) => request("/api/v1/admin/tenants", { method: "POST", body: JSON.stringify(data), }), update: (id: string, data: { name?: string }) => request(`/api/v1/admin/tenants/${id}`, { method: "PATCH", body: JSON.stringify(data), }), }, me: { get: () => request("/api/v1/me"), tenants: () => request("/api/v1/me/tenants"), }, onboarding: { createTenant: (data: OnboardingTenantCreateInput) => request("/api/v1/onboarding/tenant", { method: "POST", body: JSON.stringify(data), }), updateBranding: (tenantId: string, data: OnboardingBrandingInput) => request(`/api/v1/onboarding/tenant/${tenantId}/branding`, { method: "PATCH", body: JSON.stringify(data), }), updateDomain: (tenantId: string, data: OnboardingDomainInput) => request(`/api/v1/onboarding/tenant/${tenantId}/domain`, { method: "PATCH", body: JSON.stringify(data), }), complete: (tenantId: string) => request(`/api/v1/onboarding/tenant/${tenantId}/complete`, { method: "POST", }), }, tenantContext: { current: () => request("/api/v1/tenant/current"), switchTenant: (tenantId: string) => request("/api/v1/tenant/switch", { method: "POST", body: JSON.stringify({ tenant_id: tenantId }), }), }, // مدیریت پلتفرمی tenantها (نیازمند نقش platform_admin) platformTenants: { list: (page = 1, pageSize = 50) => request>(`/api/v1/tenants?page=${page}&page_size=${pageSize}`), get: (id: string) => request(`/api/v1/tenants/${id}`), create: (data: { name: string; slug: string; owner_user_id?: string | null; custom_domain?: string | null; }) => request("/api/v1/tenants", { method: "POST", body: JSON.stringify(data), }), update: ( id: string, data: { name?: string; status?: string; owner_user_id?: string | null } ) => request(`/api/v1/tenants/${id}`, { method: "PATCH", body: JSON.stringify(data), }), suspend: (id: string) => request(`/api/v1/tenants/${id}/suspend`, { method: "POST" }), activate: (id: string) => request(`/api/v1/tenants/${id}/activate`, { method: "POST" }), }, plans: { list: (page = 1, pageSize = 50) => request>(`/api/v1/plans?page=${page}&page_size=${pageSize}`), create: (data: { code: string; name: string; description?: string | null; is_active?: boolean; }) => request("/api/v1/plans", { method: "POST", body: JSON.stringify(data), }), attachFeature: ( planId: string, data: { feature_id: string; limit_value?: number | null; limit_period?: LimitPeriod | null; is_enabled?: boolean; } ) => request(`/api/v1/plans/${planId}/features`, { method: "POST", body: JSON.stringify(data), }), }, features: { list: (page = 1, pageSize = 100) => request>(`/api/v1/features?page=${page}&page_size=${pageSize}`), create: (data: { feature_key: string; name: string; description?: string | null; service_key: string; is_active?: boolean; }) => request("/api/v1/features", { method: "POST", body: JSON.stringify(data), }), }, domains: { listForTenant: (tenantId: string) => request(`/api/v1/tenants/${tenantId}/domains`), create: ( tenantId: string, data: { domain: string; domain_type?: "subdomain" | "custom_domain" } ) => request(`/api/v1/tenants/${tenantId}/domains`, { method: "POST", body: JSON.stringify(data), }), }, subscriptions: { get: (tenantId: string) => request(`/api/v1/tenants/${tenantId}/subscription`), create: ( tenantId: string, data: { plan_id: string; status?: SubscriptionStatus; starts_at?: string | null; ends_at?: string | null; trial_ends_at?: string | null; } ) => request(`/api/v1/tenants/${tenantId}/subscription`, { method: "POST", body: JSON.stringify(data), }), checkFeature: (tenantId: string, featureKey: string) => request(`/api/v1/tenants/${tenantId}/features/check`, { method: "POST", body: JSON.stringify({ feature_key: featureKey }), }), }, services: { list: (page = 1, pageSize = 100) => request>(`/api/v1/services?page=${page}&page_size=${pageSize}`), create: (data: { service_key: string; name: string; base_url?: string | null; health_check_url?: string | null; status?: ServiceStatus; }) => request("/api/v1/services", { method: "POST", body: JSON.stringify(data), }), updateStatus: (serviceId: string, status: ServiceStatus) => request(`/api/v1/services/${serviceId}/status`, { method: "PATCH", body: JSON.stringify({ status }), }), }, };