ceo-web/src/components/session-provider.tsx
valentinbvro 883ff370cd feat: multi-tenant dashboard shell -- auth, onboarding, organizations, tasks, team
- login page (Supabase email/password sign-in + sign-up)
- onboarding: create first tenant, redirects into /dashboard once a
  membership exists
- dashboard shell: sidebar nav, tenant switcher (x-tenant-id header,
  never in body/query -- matches ceo-api's TenantGuard), sign out
- Organizations, Tasks, and Team (members/RBAC) pages wired to ceo-api
  via a thin apiFetch wrapper that attaches the Supabase bearer token
- executive "paper & ink" design tokens (Tailwind) replacing defaults
- Dockerfile: declare NEXT_PUBLIC_* as build ARGs so Coolify's existing
  build-time env vars actually get inlined (was previously silently
  dropped, which would have crashed prerendering)
2026-07-28 21:16:17 +02:00

85 lines
3 KiB
TypeScript

'use client';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { Session } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase';
import { ACTIVE_TENANT_STORAGE_KEY, apiFetch, type MeResponse, type TenantMembership } from '../lib/api';
interface SessionContextValue {
authSession: Session | null;
authLoading: boolean;
me: MeResponse | null;
meLoading: boolean;
activeTenant: TenantMembership | null;
setActiveTenantId: (tenantId: string) => void;
signOut: () => Promise<void>;
}
const SessionContext = createContext<SessionContextValue | null>(null);
export function SessionProvider({ children }: { children: React.ReactNode }) {
const router = useRouter();
const queryClient = useQueryClient();
const [authSession, setAuthSession] = useState<Session | null>(null);
const [authLoading, setAuthLoading] = useState(true);
const [activeTenantId, setActiveTenantIdState] = useState<string | null>(null);
useEffect(() => {
supabase.auth.getSession().then(({ data }) => {
setAuthSession(data.session);
setAuthLoading(false);
});
const { data: subscription } = supabase.auth.onAuthStateChange((_event, session) => {
setAuthSession(session);
});
setActiveTenantIdState(window.localStorage.getItem(ACTIVE_TENANT_STORAGE_KEY));
return () => subscription.subscription.unsubscribe();
}, []);
const { data: me = null, isLoading: meLoading } = useQuery({
queryKey: ['me', authSession?.user.id],
queryFn: () => apiFetch<MeResponse>('/v1/me'),
enabled: Boolean(authSession),
});
const activeTenant = useMemo(() => {
if (!me || me.tenants.length === 0) {
return null;
}
return me.tenants.find((tenant) => tenant.tenantId === activeTenantId) ?? me.tenants[0];
}, [me, activeTenantId]);
const setActiveTenantId = useCallback(
(tenantId: string) => {
window.localStorage.setItem(ACTIVE_TENANT_STORAGE_KEY, tenantId);
setActiveTenantIdState(tenantId);
// datele afisate apartin tenantului anterior — invalidam tot ce e tenant-scoped
queryClient.invalidateQueries();
},
[queryClient],
);
const signOut = useCallback(async () => {
await supabase.auth.signOut();
window.localStorage.removeItem(ACTIVE_TENANT_STORAGE_KEY);
queryClient.clear();
router.push('/login');
}, [queryClient, router]);
const value = useMemo(
() => ({ authSession, authLoading, me, meLoading, activeTenant, setActiveTenantId, signOut }),
[authSession, authLoading, me, meLoading, activeTenant, setActiveTenantId, signOut],
);
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
}
export function useSession(): SessionContextValue {
const context = useContext(SessionContext);
if (!context) {
throw new Error('useSession must be used inside SessionProvider');
}
return context;
}