Build the RTL Next.js experience with live catalog, journal, customer interactions, and SEO-ready canonical routes. Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
3.3 KiB
TypeScript
142 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import { MESHKEE_API_BASE } from "@/lib/meshkee";
|
|
|
|
export type AuthUser = {
|
|
id?: string | number;
|
|
firstName?: string;
|
|
lastName?: string;
|
|
name?: string;
|
|
cellNumber?: string;
|
|
email?: string;
|
|
role?: string;
|
|
roles?: string[];
|
|
isAdmin?: boolean;
|
|
};
|
|
|
|
type AuthContextValue = {
|
|
user: AuthUser | null;
|
|
token: string | null;
|
|
loading: boolean;
|
|
isAdmin: boolean;
|
|
logout: () => void;
|
|
};
|
|
|
|
const AuthContext = createContext<AuthContextValue>({
|
|
user: null,
|
|
token: null,
|
|
loading: true,
|
|
isAdmin: false,
|
|
logout: () => undefined,
|
|
});
|
|
|
|
const tokenKeys = [
|
|
"accessToken",
|
|
"meshkeeAccessToken",
|
|
"meshkee_access_token",
|
|
"meshkee-token",
|
|
];
|
|
|
|
function readToken() {
|
|
try {
|
|
for (const key of tokenKeys) {
|
|
const value = window.localStorage.getItem(key);
|
|
if (value) return value;
|
|
}
|
|
|
|
const authValue = window.localStorage.getItem("meshkee-auth");
|
|
if (authValue) {
|
|
const auth = JSON.parse(authValue) as { accessToken?: string };
|
|
if (auth.accessToken) return auth.accessToken;
|
|
}
|
|
|
|
const cookies = document.cookie.split(";").map((item) => item.trim());
|
|
for (const key of tokenKeys) {
|
|
const prefix = `${key}=`;
|
|
const cookie = cookies.find((item) => item.startsWith(prefix));
|
|
if (cookie) return decodeURIComponent(cookie.slice(prefix.length));
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function clearTokens() {
|
|
try {
|
|
tokenKeys.forEach((key) => {
|
|
window.localStorage.removeItem(key);
|
|
document.cookie = `${key}=; Max-Age=0; path=/; domain=.glassmond.com`;
|
|
document.cookie = `${key}=; Max-Age=0; path=/`;
|
|
});
|
|
window.localStorage.removeItem("meshkee-auth");
|
|
} catch {
|
|
// External customer portal remains the source of truth for authentication.
|
|
}
|
|
}
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
const [token, setToken] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const accessToken = readToken();
|
|
if (!accessToken) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setToken(accessToken);
|
|
fetch(`${MESHKEE_API_BASE}/auth/me`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
})
|
|
.then(async (response) => {
|
|
if (!response.ok) throw new Error("Unauthorized");
|
|
return response.json() as Promise<{ user: AuthUser }>;
|
|
})
|
|
.then((data) => setUser(data.user))
|
|
.catch(() => {
|
|
clearTokens();
|
|
setToken(null);
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const logout = useCallback(() => {
|
|
clearTokens();
|
|
setUser(null);
|
|
setToken(null);
|
|
}, []);
|
|
|
|
const isAdmin = useMemo(() => {
|
|
const roles = [user?.role, ...(user?.roles ?? [])]
|
|
.filter(Boolean)
|
|
.map((role) => String(role).toLowerCase());
|
|
return Boolean(
|
|
user?.isAdmin ||
|
|
roles.some((role) => ["admin", "owner", "superadmin", "business_admin"].includes(role)),
|
|
);
|
|
}, [user]);
|
|
|
|
return (
|
|
<AuthContext.Provider value={{ user, token, loading, isAdmin, logout }}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAuth() {
|
|
return useContext(AuthContext);
|
|
}
|