From 8b17a175c3e7e27b789812eba4e3cd760beadb10 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
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/AuthContext.tsx')

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;
+}
-- 
cgit v1.2.3

