summaryrefslogblamecommitdiff
path: root/makima/frontend/src/contexts/AuthContext.tsx
blob: 809c98a1fb39cb97c27dde47c7cfb41cec48c896 (plain) (tree)
1
2
3
4
5
6
7
8
9







                 
                                                                                                    


































                                                                                       































                                                                                  







                                                                                    















                                                                        
                                                                                     


                                                                        
                                                                    



                                                                                





























                                                                               
import {
  createContext,
  useContext,
  useEffect,
  useState,
  useCallback,
  type ReactNode,
} from "react";
import { supabase, isAuthConfigured, SUPABASE_URL, 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<void>;
  /** Sign in with OAuth provider */
  signInWithOAuth: (provider: "github" | "google") => Promise<{ error: Error | null }>;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [state, setState] = useState<AuthState>({
    user: null,
    session: null,
    isLoading: true,
    isAuthenticated: false,
    isAuthConfigured: isAuthConfigured(),
  });

  // Initialize auth state
  useEffect(() => {
    // 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 }> => {
      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 }> => {
      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-${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)
    await supabase.auth.signOut({ scope: 'local' }).catch(() => {});
  }, []);

  const signInWithOAuth = useCallback(
    async (provider: "github" | "google"): Promise<{ error: Error | null }> => {
      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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth(): AuthContextValue {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}