Initial Next.js clone of Pars Elevator (plift.ir).
RTL Meshkee storefront with home/about/contact/products/blog, API auth/contact/blog wiring, and SEO sitemap-config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { getMe, getTenant, refreshAuth } from "@/lib/meshkee";
|
||||
import { BUSINESS_URL, CUSTOMER_URL, TOKEN_KEYS } from "@/lib/config";
|
||||
import { displayName, isWebsiteAdmin } from "@/lib/format";
|
||||
import type { MeshkeeUser } from "@/lib/types";
|
||||
|
||||
type AuthContextValue = {
|
||||
user: MeshkeeUser | null;
|
||||
loading: boolean;
|
||||
isAdmin: boolean;
|
||||
label: string;
|
||||
customerUrl: string;
|
||||
businessUrl: string;
|
||||
profileUrl: string;
|
||||
loginUrl: string;
|
||||
logout: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
function readToken(key: string) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeTokens(access?: string | null, refresh?: string | null) {
|
||||
try {
|
||||
if (access) localStorage.setItem(TOKEN_KEYS.access, access);
|
||||
else localStorage.removeItem(TOKEN_KEYS.access);
|
||||
if (refresh) localStorage.setItem(TOKEN_KEYS.refresh, refresh);
|
||||
else localStorage.removeItem(TOKEN_KEYS.refresh);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function captureTokensFromUrl() {
|
||||
if (typeof window === "undefined") return;
|
||||
const url = new URL(window.location.href);
|
||||
const hash = new URLSearchParams(url.hash.replace(/^#/, ""));
|
||||
const access =
|
||||
url.searchParams.get("accessToken") ||
|
||||
url.searchParams.get("token") ||
|
||||
hash.get("accessToken") ||
|
||||
hash.get("token");
|
||||
const refresh =
|
||||
url.searchParams.get("refreshToken") || hash.get("refreshToken");
|
||||
|
||||
if (!access && !refresh) return;
|
||||
|
||||
writeTokens(access, refresh);
|
||||
url.searchParams.delete("accessToken");
|
||||
url.searchParams.delete("refreshToken");
|
||||
url.searchParams.delete("token");
|
||||
url.hash = "";
|
||||
window.history.replaceState({}, "", `${url.pathname}${url.search}`);
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<MeshkeeUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [businessId, setBusinessId] = useState<string | number | null>(null);
|
||||
|
||||
const loginUrl = useMemo(() => {
|
||||
const returnTo =
|
||||
typeof window !== "undefined"
|
||||
? window.location.origin
|
||||
: "https://www.plift.ir";
|
||||
return `${CUSTOMER_URL}?redirect=${encodeURIComponent(returnTo)}`;
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
captureTokensFromUrl();
|
||||
const access = readToken(TOKEN_KEYS.access);
|
||||
const refreshToken = readToken(TOKEN_KEYS.refresh);
|
||||
|
||||
if (!access && !refreshToken) {
|
||||
setUser(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let token = access;
|
||||
if (!token && refreshToken) {
|
||||
const refreshed = await refreshAuth(refreshToken);
|
||||
writeTokens(refreshed.accessToken, refreshed.refreshToken);
|
||||
token = refreshed.accessToken;
|
||||
if (refreshed.user) {
|
||||
setUser(refreshed.user);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!token) {
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
const me = await getMe(token);
|
||||
setUser(me);
|
||||
} catch {
|
||||
if (refreshToken) {
|
||||
try {
|
||||
const refreshed = await refreshAuth(refreshToken);
|
||||
writeTokens(refreshed.accessToken, refreshed.refreshToken);
|
||||
const me = refreshed.user || (await getMe(refreshed.accessToken));
|
||||
setUser(me);
|
||||
return;
|
||||
} catch {
|
||||
writeTokens(null, null);
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
writeTokens(null, null);
|
||||
setUser(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const tenant = await getTenant();
|
||||
if (!cancelled) setBusinessId(tenant.id);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (!cancelled) await refresh();
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
writeTokens(null, null);
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
const value: AuthContextValue = {
|
||||
user,
|
||||
loading,
|
||||
isAdmin: user ? isWebsiteAdmin(user, businessId) : false,
|
||||
label: user ? `${displayName(user)} عزیز` : "",
|
||||
customerUrl: CUSTOMER_URL,
|
||||
businessUrl: BUSINESS_URL,
|
||||
profileUrl: `${CUSTOMER_URL}/profile`,
|
||||
loginUrl,
|
||||
logout,
|
||||
refresh,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user