project init

This commit is contained in:
2026-02-06 02:17:59 +03:00
commit b9d9b9ed17
129 changed files with 22835 additions and 0 deletions

View File

@ -0,0 +1,160 @@
'use client';
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { User, Session } from '@supabase/supabase-js';
import { getSupabase } from '@/lib/supabase';
import { useRouter } from 'next/navigation';
import { api, setApiToken } from '@/lib/api';
interface AuthContextType {
user: User | null;
session: Session | null;
loading: boolean;
signUp: (email: string, password: string, name: string) => Promise<{ error: Error | null }>;
signIn: (email: string, password: string) => Promise<{ error: Error | null }>;
signOut: () => Promise<void>;
getAccessToken: () => Promise<string | null>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
const supabase = getSupabase();
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setUser(session?.user ?? null);
// Only set loading false when there's no session; otherwise wait for token exchange
if (!session) {
setApiToken(null);
setLoading(false);
}
});
// Listen for auth changes
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
setUser(session?.user ?? null);
if (!session) {
setApiToken(null);
setLoading(false);
}
});
return () => subscription.unsubscribe();
}, [supabase.auth]);
const runExchange = useCallback(() => {
if (!session?.access_token) {
setApiToken(null);
return;
}
api
.exchangeToken(session.access_token)
.then(({ accessToken }) => {
setApiToken(accessToken);
setLoading(false);
})
.catch(() => {
setApiToken(null);
setLoading(false);
});
}, [session?.access_token]);
// Exchange Supabase token for backend JWT; keep loading true until done so API calls wait for JWT
useEffect(() => {
runExchange();
}, [runExchange]);
// Re-exchange on 401 (e.g. JWT expired) so next request gets a fresh token
useEffect(() => {
const handler = () => {
if (session?.access_token) runExchange();
};
window.addEventListener('auth:unauthorized', handler);
return () => window.removeEventListener('auth:unauthorized', handler);
}, [session?.access_token, runExchange]);
const signUp = useCallback(
async (email: string, password: string, name: string) => {
try {
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
full_name: name,
},
},
});
if (error) throw error;
return { error: null };
} catch (error) {
return { error: error as Error };
}
},
[supabase.auth]
);
const signIn = useCallback(
async (email: string, password: string) => {
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
return { error: null };
} catch (error) {
return { error: error as Error };
}
},
[supabase.auth]
);
const signOut = useCallback(async () => {
await supabase.auth.signOut();
router.push('/');
}, [supabase.auth, router]);
const getAccessToken = useCallback(async () => {
const { data } = await supabase.auth.getSession();
return data.session?.access_token ?? null;
}, [supabase.auth]);
return (
<AuthContext.Provider
value={{
user,
session,
loading,
signUp,
signIn,
signOut,
getAccessToken,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}