170 lines
4.0 KiB
TypeScript
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;
|
|
}
|