176 lines
4.5 KiB
TypeScript
176 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import {
|
|
clearSharedAuthCookies,
|
|
hydrateLocalTokensFromSharedCookies,
|
|
SHARED_ACCESS_TOKEN_KEY,
|
|
SHARED_REFRESH_TOKEN_KEY,
|
|
writeSharedAuthCookies,
|
|
} from "./shared-auth-cookie";
|
|
|
|
export type AuthBusinessMembership = {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
isOwner: boolean;
|
|
teamRole: string | null;
|
|
};
|
|
|
|
export type AuthUser = {
|
|
id: string;
|
|
firstName?: string;
|
|
lastName?: string;
|
|
cellNumber?: string;
|
|
email?: string;
|
|
roles?: string[];
|
|
isSuperAdmin?: boolean;
|
|
businesses?: AuthBusinessMembership[];
|
|
};
|
|
|
|
type SessionContextValue = {
|
|
user: AuthUser | null;
|
|
isAuthenticated: boolean;
|
|
ready: boolean;
|
|
businessId: string;
|
|
isWebsiteAdmin: boolean;
|
|
logout: () => void;
|
|
getAccessToken: () => string | null;
|
|
};
|
|
|
|
function normalizeUser(raw: Partial<AuthUser> & { id?: string | number }): AuthUser | null {
|
|
if (raw.id == null) return null;
|
|
return {
|
|
id: String(raw.id),
|
|
firstName: raw.firstName ?? undefined,
|
|
lastName: raw.lastName ?? undefined,
|
|
cellNumber: raw.cellNumber ?? undefined,
|
|
email: raw.email ?? undefined,
|
|
roles: Array.isArray(raw.roles) ? raw.roles : undefined,
|
|
isSuperAdmin: Boolean(raw.isSuperAdmin),
|
|
businesses: Array.isArray(raw.businesses)
|
|
? raw.businesses.map((business) => ({
|
|
id: String(business.id),
|
|
name: business.name,
|
|
slug: business.slug,
|
|
isOwner: Boolean(business.isOwner),
|
|
teamRole: business.teamRole ?? null,
|
|
}))
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
export function isWebsiteAdminForBusiness(user: AuthUser | null | undefined, businessId: string): boolean {
|
|
if (!user || !businessId) return false;
|
|
if (user.isSuperAdmin || user.roles?.includes("super_admin")) return true;
|
|
return Boolean(
|
|
user.businesses?.some(
|
|
(business) => String(business.id) === String(businessId) && (business.isOwner || Boolean(business.teamRole)),
|
|
),
|
|
);
|
|
}
|
|
|
|
const SessionContext = createContext<SessionContextValue | null>(null);
|
|
|
|
function readToken(key: string): string | null {
|
|
try {
|
|
return localStorage.getItem(key);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writeToken(key: string, value: string | null) {
|
|
try {
|
|
if (value) localStorage.setItem(key, value);
|
|
else localStorage.removeItem(key);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
export function SessionProvider({
|
|
children,
|
|
businessId,
|
|
}: {
|
|
children: ReactNode;
|
|
businessId: string;
|
|
}) {
|
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
const [ready, setReady] = useState(false);
|
|
|
|
const getAccessToken = useCallback(() => {
|
|
hydrateLocalTokensFromSharedCookies(SHARED_ACCESS_TOKEN_KEY, SHARED_REFRESH_TOKEN_KEY);
|
|
return readToken(SHARED_ACCESS_TOKEN_KEY);
|
|
}, []);
|
|
|
|
const clearTokens = useCallback(() => {
|
|
writeToken(SHARED_ACCESS_TOKEN_KEY, null);
|
|
writeToken(SHARED_REFRESH_TOKEN_KEY, null);
|
|
writeToken("accessToken", null);
|
|
writeToken("meshkee_access_token", null);
|
|
clearSharedAuthCookies();
|
|
setUser(null);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const token = getAccessToken();
|
|
if (!token) {
|
|
setReady(true);
|
|
return;
|
|
}
|
|
|
|
const refresh = readToken(SHARED_REFRESH_TOKEN_KEY);
|
|
writeSharedAuthCookies(token, refresh ?? undefined);
|
|
|
|
fetch("/api/auth/me", { headers: { Authorization: `Bearer ${token}` } })
|
|
.then(async (res) => {
|
|
if (res.status === 401) {
|
|
clearTokens();
|
|
return;
|
|
}
|
|
if (!res.ok) return;
|
|
const data = (await res.json()) as { user?: AuthUser } & AuthUser;
|
|
const next = normalizeUser(data.user ?? data);
|
|
if (next) setUser(next);
|
|
})
|
|
.catch(() => undefined)
|
|
.finally(() => setReady(true));
|
|
}, [clearTokens, getAccessToken]);
|
|
|
|
const logout = useCallback(() => {
|
|
clearTokens();
|
|
}, [clearTokens]);
|
|
|
|
const isWebsiteAdmin = useMemo(() => isWebsiteAdminForBusiness(user, businessId), [user, businessId]);
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
user,
|
|
isAuthenticated: Boolean(user),
|
|
ready,
|
|
businessId,
|
|
isWebsiteAdmin,
|
|
logout,
|
|
getAccessToken,
|
|
}),
|
|
[user, ready, businessId, isWebsiteAdmin, logout, getAccessToken],
|
|
);
|
|
|
|
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
|
|
}
|
|
|
|
export function useSession() {
|
|
const ctx = useContext(SessionContext);
|
|
if (!ctx) throw new Error("useSession must be used within SessionProvider");
|
|
return ctx;
|
|
}
|