Add Oudelaa dashboard API integration
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
175
oudelaa_dashboard/components/auth/auth-guard.tsx
Normal file
175
oudelaa_dashboard/components/auth/auth-guard.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
import { SuperAdminSessionProvider } from "@/components/auth/session-context";
|
||||
import { getSuperAdminSession } from "@/lib/api/superadmin";
|
||||
import { refreshSuperAdmin } from "@/lib/auth/client";
|
||||
import type { SuperAdminSessionResponse } from "@/types/api";
|
||||
|
||||
const RETRY_DELAY_MS = 5000;
|
||||
|
||||
function isRecoverableSessionError(error: unknown) {
|
||||
const message = String(error).toLowerCase();
|
||||
return [
|
||||
"502",
|
||||
"503",
|
||||
"504",
|
||||
"fetch failed",
|
||||
"failed to fetch",
|
||||
"econnrefused",
|
||||
"timeout",
|
||||
"network",
|
||||
"upstream",
|
||||
].some((token) => message.includes(token));
|
||||
}
|
||||
|
||||
export function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [sessionError, setSessionError] = useState<string | null>(null);
|
||||
const [session, setSession] = useState<SuperAdminSessionResponse | null>(null);
|
||||
const [retryNonce, setRetryNonce] = useState(0);
|
||||
const [retryCountdown, setRetryCountdown] = useState<number | null>(null);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let retryTimeout: number | null = null;
|
||||
let countdownInterval: number | null = null;
|
||||
|
||||
const clearRetryTimers = () => {
|
||||
if (retryTimeout) {
|
||||
window.clearTimeout(retryTimeout);
|
||||
retryTimeout = null;
|
||||
}
|
||||
|
||||
if (countdownInterval) {
|
||||
window.clearInterval(countdownInterval);
|
||||
countdownInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleRetry = () => {
|
||||
clearRetryTimers();
|
||||
setRetryCountdown(RETRY_DELAY_MS / 1000);
|
||||
|
||||
countdownInterval = window.setInterval(() => {
|
||||
setRetryCountdown((value) => (value && value > 1 ? value - 1 : 1));
|
||||
}, 1000);
|
||||
|
||||
retryTimeout = window.setTimeout(() => {
|
||||
clearRetryTimers();
|
||||
setRetryCountdown(null);
|
||||
if (active) {
|
||||
setRetryNonce((value) => value + 1);
|
||||
}
|
||||
}, RETRY_DELAY_MS);
|
||||
};
|
||||
|
||||
const ensureSession = async () => {
|
||||
try {
|
||||
const nextSession = await getSuperAdminSession();
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearRetryTimers();
|
||||
setSession(nextSession);
|
||||
setSessionError(null);
|
||||
setRetryCountdown(null);
|
||||
setReady(true);
|
||||
} catch (initialError) {
|
||||
try {
|
||||
const refreshed = await refreshSuperAdmin();
|
||||
if (!refreshed) {
|
||||
router.replace("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSession = await getSuperAdminSession();
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearRetryTimers();
|
||||
setSession(nextSession);
|
||||
setSessionError(null);
|
||||
setRetryCountdown(null);
|
||||
setReady(true);
|
||||
} catch (error) {
|
||||
if (String(initialError).includes("401") || String(error).includes("401")) {
|
||||
router.replace("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSession(null);
|
||||
setSessionError(String(error));
|
||||
setReady(true);
|
||||
|
||||
if (isRecoverableSessionError(initialError) || isRecoverableSessionError(error)) {
|
||||
scheduleRetry();
|
||||
} else {
|
||||
clearRetryTimers();
|
||||
setRetryCountdown(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void ensureSession();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
clearRetryTimers();
|
||||
};
|
||||
}, [router, pathname, retryNonce]);
|
||||
|
||||
if (!ready) {
|
||||
return (
|
||||
<div className="frame-panel mx-auto mt-10 w-full max-w-2xl p-6 text-sm text-muted-foreground">
|
||||
Checking the current SuperAdmin session...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sessionError && !session) {
|
||||
return (
|
||||
<SuperAdminSessionProvider value={{ session: null, permissions: [] }}>
|
||||
<div className="frame-panel mx-auto mt-10 w-full max-w-2xl space-y-4 p-6 text-sm text-muted-foreground">
|
||||
<div>
|
||||
The dashboard could not verify the current session, so permissions were not loaded.
|
||||
Reload the page after the backend connection is available again.
|
||||
</div>
|
||||
<div className="break-words text-xs text-red-300">{sessionError}</div>
|
||||
{retryCountdown !== null ? (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Retrying automatically in {retryCountdown}s...
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-border px-4 py-2 text-foreground transition hover:bg-secondary"
|
||||
onClick={() => {
|
||||
setRetryCountdown(null);
|
||||
setRetryNonce((value) => value + 1);
|
||||
}}
|
||||
>
|
||||
Retry now
|
||||
</button>
|
||||
</div>
|
||||
</SuperAdminSessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SuperAdminSessionProvider value={{ session, permissions: session?.permissions ?? [] }}>
|
||||
{children}
|
||||
</SuperAdminSessionProvider>
|
||||
);
|
||||
}
|
||||
18
oudelaa_dashboard/components/auth/no-permission-state.tsx
Normal file
18
oudelaa_dashboard/components/auth/no-permission-state.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
export function NoPermissionState({
|
||||
description,
|
||||
title = "Access restricted",
|
||||
}: {
|
||||
description: string;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<EmptyState title={title} description={description} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
33
oudelaa_dashboard/components/auth/session-context.tsx
Normal file
33
oudelaa_dashboard/components/auth/session-context.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import type { SuperAdminSessionResponse } from "@/types/api";
|
||||
|
||||
type SuperAdminSessionContextValue = {
|
||||
session: SuperAdminSessionResponse | null;
|
||||
permissions: string[];
|
||||
};
|
||||
|
||||
const SuperAdminSessionContext = createContext<SuperAdminSessionContextValue>({
|
||||
session: null,
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
export function SuperAdminSessionProvider({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: SuperAdminSessionContextValue;
|
||||
}) {
|
||||
return (
|
||||
<SuperAdminSessionContext.Provider value={value}>
|
||||
{children}
|
||||
</SuperAdminSessionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSuperAdminSession() {
|
||||
return useContext(SuperAdminSessionContext);
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم