summaryrefslogtreecommitdiff
path: root/makima/frontend/src/contexts/AuthContext.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/contexts/AuthContext.tsx')
-rw-r--r--makima/frontend/src/contexts/AuthContext.tsx160
1 files changed, 160 insertions, 0 deletions
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<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(() => {
+ 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 <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;
+}