diff options
| author | soryu <soryu@soryu.co> | 2026-01-06 04:08:11 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-11 03:01:13 +0000 |
| commit | 8b17a175c3e7e27b789812eba4e3cd760beadb10 (patch) | |
| tree | 7864dcaa2fa9db47fdfd4e8bfdb0b1dde832aa33 /makima/frontend/src/contexts/AuthContext.tsx | |
| parent | f79c416c58557d2f946aa5332989afdfa8c021cd (diff) | |
| download | soryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.tar.gz soryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.zip | |
Initial Control system
Diffstat (limited to 'makima/frontend/src/contexts/AuthContext.tsx')
| -rw-r--r-- | makima/frontend/src/contexts/AuthContext.tsx | 160 |
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; +} |
