From 8b17a175c3e7e27b789812eba4e3cd760beadb10 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 6 Jan 2026 04:08:11 +0000 Subject: Initial Control system --- makima/frontend/src/contexts/AuthContext.tsx | 160 +++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 makima/frontend/src/contexts/AuthContext.tsx (limited to 'makima/frontend/src/contexts') diff --git a/makima/frontend/src/contexts/AuthContext.tsx b/makima/frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..ce2724b --- /dev/null +++ b/makima/frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,160 @@ +import { + createContext, + useContext, + useEffect, + useState, + useCallback, + type ReactNode, +} from "react"; +import { supabase, isAuthConfigured, type Session, type User } from "../lib/supabase"; + +interface AuthState { + user: User | null; + session: Session | null; + isLoading: boolean; + isAuthenticated: boolean; + isAuthConfigured: boolean; +} + +interface AuthContextValue extends AuthState { + /** Get the current access token for API calls */ + getAccessToken: () => string | null; + /** Sign in with email and password */ + signIn: (email: string, password: string) => Promise<{ error: Error | null }>; + /** Sign up with email and password */ + signUp: (email: string, password: string) => Promise<{ error: Error | null }>; + /** Sign out */ + signOut: () => Promise; + /** Sign in with OAuth provider */ + signInWithOAuth: (provider: "github" | "google") => Promise<{ error: Error | null }>; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [state, setState] = useState({ + user: null, + session: null, + isLoading: true, + isAuthenticated: false, + isAuthConfigured: isAuthConfigured(), + }); + + // Initialize auth state + useEffect(() => { + if (!supabase) { + // Auth not configured - allow unauthenticated access + setState((prev) => ({ + ...prev, + isLoading: false, + isAuthenticated: true, // Allow access when auth is not configured + })); + return; + } + + // Get initial session + supabase.auth.getSession().then(({ data: { session } }) => { + setState({ + user: session?.user ?? null, + session, + isLoading: false, + isAuthenticated: !!session, + isAuthConfigured: true, + }); + }); + + // Listen for auth changes + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setState((prev) => ({ + ...prev, + user: session?.user ?? null, + session, + isAuthenticated: !!session, + })); + }); + + return () => subscription.unsubscribe(); + }, []); + + const getAccessToken = useCallback((): string | null => { + return state.session?.access_token ?? null; + }, [state.session]); + + const signIn = useCallback( + async (email: string, password: string): Promise<{ error: Error | null }> => { + if (!supabase) { + return { error: new Error("Auth not configured") }; + } + const { error } = await supabase.auth.signInWithPassword({ email, password }); + return { error: error ? new Error(error.message) : null }; + }, + [] + ); + + const signUp = useCallback( + async (email: string, password: string): Promise<{ error: Error | null }> => { + if (!supabase) { + return { error: new Error("Auth not configured") }; + } + const { error } = await supabase.auth.signUp({ email, password }); + return { error: error ? new Error(error.message) : null }; + }, + [] + ); + + const signOut = useCallback(async () => { + // Always clear local state first + setState((prev) => ({ + ...prev, + user: null, + session: null, + isAuthenticated: false, + })); + + // Clear Supabase storage directly in case signOut API fails + const storageKey = `sb-${import.meta.env.VITE_SUPABASE_URL?.split('//')[1]?.split('.')[0]}-auth-token`; + localStorage.removeItem(storageKey); + + // Try to call signOut API (may fail if token is invalid, that's OK) + if (supabase) { + await supabase.auth.signOut({ scope: 'local' }).catch(() => {}); + } + }, []); + + const signInWithOAuth = useCallback( + async (provider: "github" | "google"): Promise<{ error: Error | null }> => { + if (!supabase) { + return { error: new Error("Auth not configured") }; + } + const { error } = await supabase.auth.signInWithOAuth({ + provider, + options: { + redirectTo: window.location.origin, + }, + }); + return { error: error ? new Error(error.message) : null }; + }, + [] + ); + + const value: AuthContextValue = { + ...state, + getAccessToken, + signIn, + signUp, + signOut, + signInWithOAuth, + }; + + return {children}; +} + +export function useAuth(): AuthContextValue { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} -- cgit v1.2.3