Files
hoomat-store/app/providers/auth-provider.tsx
T

170 lines
4.0 KiB
TypeScript

"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {
getMe,
getTenant,
refreshTokens,
} from "@/lib/meshkee/api";
import {
ACCESS_TOKEN_KEY,
REFRESH_TOKEN_KEY,
} from "@/lib/meshkee/config";
import type { AuthUser } from "@/lib/meshkee/types";
type AuthContextValue = {
user: AuthUser | null;
isAuthenticated: boolean;
ready: boolean;
businessId: string | null;
isWebsiteAdmin: boolean;
getAccessToken: () => string | null;
logout: () => void;
refreshUser: () => Promise<void>;
};
const AuthContext = createContext<AuthContextValue | null>(null);
function readToken(key: string) {
if (typeof window === "undefined") return null;
return localStorage.getItem(key);
}
function writeTokens(accessToken: string, refreshToken: string) {
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
}
function clearTokens() {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [ready, setReady] = useState(false);
const [businessId, setBusinessId] = useState<string | null>(null);
const logout = useCallback(() => {
clearTokens();
setUser(null);
}, []);
const getAccessToken = useCallback(() => readToken(ACCESS_TOKEN_KEY), []);
const refreshUser = useCallback(async () => {
let accessToken = readToken(ACCESS_TOKEN_KEY);
const refreshToken = readToken(REFRESH_TOKEN_KEY);
if (!accessToken && !refreshToken) {
setUser(null);
return;
}
try {
if (!accessToken && refreshToken) {
const tokens = await refreshTokens(refreshToken);
writeTokens(tokens.accessToken, tokens.refreshToken);
accessToken = tokens.accessToken;
}
if (!accessToken) {
setUser(null);
return;
}
const me = await getMe(accessToken);
setUser(me);
} catch {
if (refreshToken) {
try {
const tokens = await refreshTokens(refreshToken);
writeTokens(tokens.accessToken, tokens.refreshToken);
const me = await getMe(tokens.accessToken);
setUser(me);
return;
} catch {
clearTokens();
setUser(null);
}
} else {
clearTokens();
setUser(null);
}
}
}, []);
useEffect(() => {
let cancelled = false;
(async () => {
try {
try {
const tenant = await getTenant();
if (!cancelled) setBusinessId(String(tenant.id));
} catch {
if (!cancelled) setBusinessId(null);
}
await refreshUser();
} catch {
if (!cancelled) {
clearTokens();
setUser(null);
}
} finally {
if (!cancelled) setReady(true);
}
})().catch(() => {
// swallow unexpected bootstrap rejections (e.g. aborted HMR)
});
return () => {
cancelled = true;
};
}, [refreshUser]);
const isWebsiteAdmin = useMemo(() => {
if (!user || !businessId) return false;
if (user.isSuperAdmin || user.roles?.includes("super_admin")) return true;
return Boolean(
user.businesses?.some(
(biz) =>
String(biz.id) === String(businessId) &&
(biz.isOwner || Boolean(biz.teamRole)),
),
);
}, [user, businessId]);
const value = useMemo<AuthContextValue>(
() => ({
user,
isAuthenticated: Boolean(user),
ready,
businessId,
isWebsiteAdmin,
getAccessToken,
logout,
refreshUser,
}),
[
user,
ready,
businessId,
isWebsiteAdmin,
getAccessToken,
logout,
refreshUser,
],
);
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;
}