Files
course-craft-service/apps/web/src/contexts/auth-context.tsx
root 5241144bc5 feat: AI quiz generation per lesson + hide edit buttons for non-authors
- Generate unique quiz for each lesson using OpenRouter API
- Parse lesson content (TipTap JSON) and send to AI for question generation
- Cache quiz in database to avoid regeneration
- Hide Edit/Delete buttons if current user is not course author
- Add backendUser to auth context for proper authorization checks
- Show certificate button prominently when course is completed

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 11:04:37 +00:00

187 lines
4.9 KiB
TypeScript

'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 BackendUser {
id: string;
email: string;
name: string | null;
avatarUrl: string | null;
subscriptionTier: string;
}
interface AuthContextType {
user: User | null;
backendUser: BackendUser | 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 [backendUser, setBackendUser] = useState<BackendUser | 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;
}
let attempt = 0;
const maxRetries = 3;
const tryExchange = () => {
api
.exchangeToken(session.access_token)
.then(({ accessToken, user: backendUserData }) => {
setApiToken(accessToken);
setBackendUser(backendUserData);
setLoading(false);
})
.catch(() => {
attempt++;
if (attempt < maxRetries) {
// Retry with exponential backoff (500ms, 1500ms, 3500ms)
setTimeout(tryExchange, 500 * Math.pow(2, attempt));
} else {
setApiToken(null);
setLoading(false);
}
});
};
tryExchange();
}, [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,
backendUser,
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;
}