From 85a585131e6af6614d6554cf8a3c1c0725f604c3 Mon Sep 17 00:00:00 2001 From: streaper2 Date: Mon, 16 Feb 2026 22:02:45 +0100 Subject: [PATCH] login ok --- components/AuthPage.tsx | 6 +- components/LoginPage.tsx | 14 +- hooks.bak copy.ts | 385 ----------------------- hooks.ts | 656 +++++++++++++++++++-------------------- scripts/debug-project.js | 79 +++++ services/api.backup.ts | 264 ---------------- services/api.bak2.ts | 233 -------------- services/api.ts | 479 +++++++--------------------- vite.config.ts | 28 ++ 9 files changed, 551 insertions(+), 1593 deletions(-) delete mode 100644 hooks.bak copy.ts create mode 100644 scripts/debug-project.js delete mode 100644 services/api.backup.ts delete mode 100644 services/api.bak2.ts diff --git a/components/AuthPage.tsx b/components/AuthPage.tsx index 6f84cbe..6e99d42 100644 --- a/components/AuthPage.tsx +++ b/components/AuthPage.tsx @@ -31,7 +31,7 @@ const AuthPage: React.FC = ({ onBack, onSuccess, initialMode = 's setError(''); try { - const result = await login(adminData); + const result = await login(adminData.email, adminData.password); if (result?.error) setError(result.error); } catch (e) { setError('Erreur de connexion au service.'); @@ -48,9 +48,9 @@ const AuthPage: React.FC = ({ onBack, onSuccess, initialMode = 's try { let result; if (mode === 'signup') { - result = await signup(formData); + result = await signup(formData.email, formData.password, formData.name); } else { - result = await login({ email: formData.email, password: formData.password }); + result = await login(formData.email, formData.password); } if (result?.error) { diff --git a/components/LoginPage.tsx b/components/LoginPage.tsx index b6fcab9..0e19b87 100644 --- a/components/LoginPage.tsx +++ b/components/LoginPage.tsx @@ -22,16 +22,10 @@ const LoginPage: React.FC = ({ onSuccess, onRegister }) => { setLoading(true); try { - const result = await login({ email, password }); - - if (result && result.error) { - setError(result.error); - setLoading(false); - } else { - onSuccess(); - } - } catch (err) { - setError("Une erreur inattendue est survenue."); + await login(email, password); + onSuccess(); + } catch (err: any) { + setError(err.message || "Une erreur inattendue est survenue."); setLoading(false); } } diff --git a/hooks.bak copy.ts b/hooks.bak copy.ts deleted file mode 100644 index a9f755f..0000000 --- a/hooks.bak copy.ts +++ /dev/null @@ -1,385 +0,0 @@ -import { useState, useCallback, useEffect } from 'react'; -import { UserProfile, BookProject, Chapter, ChatMessage, PlanType, Entity, Idea, WorkflowData } from './types'; -import { INITIAL_CHAPTER } from './constants'; -import { generateStoryContent } from './services/geminiService'; -import { authService, dataService } from './services/api.bak2'; - -export function useAuth() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - const checkSession = useCallback(async (injectedUser: any = null) => { - setLoading(true); // On affiche l'écran de chargement - console.log("[useAuth]checkSession ==> Démarrage de la connexion complète..."); - - try { - // 1. Vérification de la session Auth - const sessionUser = injectedUser || await authService.getSession(); - - if (sessionUser) { - console.log("[useAuth] 1/2 Session validée. Récupération du profil..."); - - // 2. Récupération du profil (BLOQUANT avec await) - // On attend les 2 secondes ici pour avoir une transition propre - let profile = await dataService.getProfile(sessionUser.id); - - // 3. Si le profil n'existe pas, on le crée avant de continuer - if (!profile) { - console.log("[useAuth] Profil absent, création en cours..."); - const createResult = await dataService.createItem('profiles', { - user_id: sessionUser.id, - email: sessionUser.email, - full_name: sessionUser.name || 'Nouvel Écrivain' - // ... autres champs par défaut - }); - - // On simule l'objet profil pour éviter un second appel API - profile = { user_id: sessionUser.id, email: sessionUser.email, full_name: sessionUser.name }; - } - - // 4. On remplit l'état final - setUser({ - id: sessionUser.id, - email: sessionUser.email, - name: profile.full_name || sessionUser.name, - avatar: profile.avatar_url || `https://i.pravatar.cc/150?u=${sessionUser.email}`, - subscription: { plan: profile.subscription_plan || 'free', startDate: 0, status: 'active' }, - usage: { - aiActionsCurrent: profile.ai_actions_current || 0, - aiActionsLimit: profile.ai_actions_limit || 10, - projectsLimit: profile.projects_limit || 1 - }, - preferences: { theme: profile.theme || 'light', dailyWordGoal: 500, language: 'fr' }, - stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 }, - bio: profile.bio || "" - }); - - console.log("[useAuth] 2/2 Profil récupéré. Accès au Dashboard."); - - - } else { - console.log("[useAuth] 2/2 Profil non récupéré. Redirection vers la page de connexion."); - setUser(null); - } - } catch (err) { - console.error("[useAuth] Erreur lors de la connexion:", err); - setUser(null); - } finally { - // C'est seulement ici que l'écran de chargement disparaît - setLoading(false); - } - }, []); - - useEffect(() => { checkSession(); }, [checkSession]); - - const login = async (data: any) => { - // UI Feedback immédiat si nécessaire - setLoading(true); - - // Appel API - const result = await authService.signIn(data.email, data.password); - console.log("[useAuth] Résultat de la connexion:", result); - if (result && !result.error) { - // Utilisation directe de la réponse API pour connecter l'utilisateur - // Cela évite d'attendre le round-trip réseau de getSession - const userObj = result.user || (result.id ? result : null); - - if (userObj) { - console.log("[useAuth] Login réussi, injection immédiate."); - await checkSession(userObj); - // Note: checkSession est maintenant optimisé pour ne pas re-bloquer - } else { - await checkSession(); - } - } else { - // En cas d'erreur, on arrête le chargement pour afficher le message - setLoading(false); - } - return result; - }; - - const signup = async (data: any) => { - setLoading(true); - const result = await authService.signUp(data.email, data.password, data.name); - if (result && !result.error) { - const userObj = result.user || (result.id ? result : null); - // Injection immédiate comme pour le login - await checkSession(userObj); - } else { - setLoading(false); - } - return result; - }; - - const logout = async () => { - setLoading(true); - await authService.signOut(); - setUser(null); - setLoading(false); - }; - - const incrementUsage = async () => { - if (user) { - setUser(prev => prev ? { - ...prev, - usage: { ...prev.usage, aiActionsCurrent: prev.usage.aiActionsCurrent + 1 } - } : null); - } - }; - - return { user, login, signup, logout, incrementUsage, loading }; -} - -export function useProjects(user: UserProfile | null) { - const [projects, setProjects] = useState([]); - const [currentProjectId, setCurrentProjectId] = useState(null); - const [loading, setLoading] = useState(false); - - // DEBUG: Check if user is present - useEffect(() => { - console.log("[useProjects DEBUG] Hook initialized/updated with user:", user); - }, [user]); - - const fetchProjects = useCallback(async () => { - if (!user) { - console.log("[useProjects DEBUG] fetchProjects skipped: No user"); - return; - } - console.log("[useProjects DEBUG] fetchProjects starting for user:", user.id); - setLoading(true); - try { - const data = await dataService.getProjects(user.id); - console.log("[useProjects DEBUG] Projects fetched:", data); - setProjects(data.map((p: any) => ({ - id: p.id.toString(), - title: p.title, - author: p.author, - lastModified: Date.now(), - settings: { - genre: p.genre, - subGenre: p.sub_genre, - targetAudience: p.target_audience, - tone: p.tone, - pov: p.pov, - tense: p.tense, - synopsis: p.synopsis, - themes: p.themes - }, - styleGuide: p.style_guide, - chapters: [], - entities: [], - ideas: [], - workflow: { nodes: [], connections: [] } - }))); - } catch (e) { - console.error("[useProjects] Erreur chargement projets:", e); - } finally { - setLoading(false); - } - }, [user]); - - useEffect(() => { fetchProjects(); }, [fetchProjects]); - - const fetchProjectDetails = async (projectId: string) => { - const id = parseInt(projectId); - try { - const [chapters, entities, ideas, workflows] = await Promise.all([ - dataService.getRelatedData('chapters', id), - dataService.getRelatedData('entities', id), - dataService.getRelatedData('ideas', id), - dataService.getRelatedData('workflows', id) - ]); - - setProjects(prev => prev.map(p => p.id === projectId ? { - ...p, - chapters: chapters.map((c: any) => ({ ...c, id: c.id.toString() })), - entities: entities.map((e: any) => ({ - ...e, - id: e.id.toString(), - customValues: e.custom_values ? JSON.parse(e.custom_values) : {}, - attributes: e.attributes ? (typeof e.attributes === 'string' ? JSON.parse(e.attributes) : e.attributes) : undefined - })), - ideas: ideas.map((i: any) => ({ ...i, id: i.id.toString() })), - workflow: workflows[0] ? { - nodes: JSON.parse(workflows[0].nodes || '[]'), - connections: JSON.parse(workflows[0].connections || '[]') - } : { nodes: [], connections: [] } - } : p)); - } catch (e) { - console.error("[useProjects] Erreur détails projet:", e); - } - }; - - const createProject = async () => { - if (!user) return null; - const result = await dataService.createProject({ - user_id: user.id, - title: "Nouveau Roman", - author: user.name - }); - if (result.status === 'success') { - await fetchProjects(); - return result.id.toString(); - } - return null; - }; - - const addChapter = async () => { - if (!currentProjectId) return null; - const projectId = parseInt(currentProjectId); - const result = await dataService.createItem('chapters', { - project_id: projectId, - title: "Nouveau Chapitre", - content: "

Écrivez ici votre prochain chapitre...

", - summary: "" - }); - if (result.status === 'success' && result.id) { - await fetchProjectDetails(currentProjectId); - return result.id.toString(); - } - return null; - }; - - const createEntity = async (entity: Omit) => { - if (!currentProjectId) return null; - const projectId = parseInt(currentProjectId); - - const dbPayload = { - project_id: projectId, - type: entity.type, - name: entity.name, - description: entity.description, - details: entity.details, - story_context: entity.storyContext, - attributes: JSON.stringify(entity.attributes || {}), - custom_values: JSON.stringify(entity.customValues || {}) - }; - - const result = await dataService.createItem('entities', dbPayload); - - if (result.status === 'success') { - const newId = result.id.toString(); - const newEntity = { ...entity, id: newId }; - - setProjects(prev => prev.map(p => p.id === currentProjectId ? { - ...p, - entities: [...p.entities, newEntity] - } : p)); - return newId; - } - return null; - }; - - const updateEntity = async (entityId: string, updates: Partial) => { - if (!currentProjectId) return; - const pId = parseInt(currentProjectId); - const eId = parseInt(entityId); - - const dbPayload: any = {}; - if (updates.name !== undefined) dbPayload.name = updates.name; - if (updates.description !== undefined) dbPayload.description = updates.description; - if (updates.details !== undefined) dbPayload.details = updates.details; - if (updates.storyContext !== undefined) dbPayload.story_context = updates.storyContext; - if (updates.attributes !== undefined) dbPayload.attributes = JSON.stringify(updates.attributes); - if (updates.customValues !== undefined) dbPayload.custom_values = JSON.stringify(updates.customValues); - - if (Object.keys(dbPayload).length > 0) { - await dataService.updateItem('entities', eId, dbPayload); - } - - setProjects(prev => prev.map(p => p.id === currentProjectId ? { - ...p, - entities: p.entities.map(e => e.id === entityId ? { ...e, ...updates } : e) - } : p)); - }; - - const deleteEntity = async (entityId: string) => { - if (!currentProjectId) return; - const eId = parseInt(entityId); - - await dataService.deleteItem('entities', eId); - - setProjects(prev => prev.map(p => p.id === currentProjectId ? { - ...p, - entities: p.entities.filter(e => e.id !== entityId) - } : p)); - }; - - const updateProject = async (updates: Partial) => { - if (!currentProjectId) return; - const id = parseInt(currentProjectId); - - const ncbData: any = {}; - if (updates.title) ncbData.title = updates.title; - if (updates.author) ncbData.author = updates.author; - if (updates.settings) { - ncbData.genre = updates.settings.genre; - ncbData.sub_genre = updates.settings.subGenre; - ncbData.synopsis = updates.settings.synopsis; - ncbData.tone = updates.settings.tone; - ncbData.pov = updates.settings.pov; - ncbData.tense = updates.settings.tense; - } - // Note: workflow, ideas, templates are not yet persisted via updateProject fully if complex types - // Using separate updaters is better. - - if (Object.keys(ncbData).length > 0) { - await dataService.updateItem('projects', id, ncbData); - } - - setProjects(prev => prev.map(p => p.id === currentProjectId ? { ...p, ...updates } as BookProject : p)); - }; - - const updateChapter = async (chapterId: string, updates: Partial) => { - if (!currentProjectId) return; - const pId = parseInt(currentProjectId); - const cId = parseInt(chapterId); - - // Call API - await dataService.updateItem('chapters', cId, updates); - - // Update Local State - setProjects(prev => prev.map(p => p.id === currentProjectId ? { - ...p, - chapters: p.chapters.map(c => c.id === chapterId ? { ...c, ...updates } : c) - } : p)); - }; - - return { - projects, currentProjectId, setCurrentProjectId: (id: string | null) => { - setCurrentProjectId(id); - if (id) fetchProjectDetails(id); - }, - createProject, updateProject, updateChapter, addChapter, - createEntity, updateEntity, deleteEntity, - loading - }; -} - -export function useChat() { - const [chatHistory, setChatHistory] = useState([]); - const [isGenerating, setIsGenerating] = useState(false); - - const sendMessage = async (project: BookProject, chapterId: string, msg: string, user: UserProfile, onUsed: () => void) => { - const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text: msg }; - setChatHistory(prev => [...prev, userMsg]); - setIsGenerating(true); - - try { - const response = await generateStoryContent(project, chapterId, msg, user, onUsed); - const aiMsg: ChatMessage = { - id: (Date.now() + 1).toString(), - role: 'model', - text: response.text, - responseType: response.type - }; - setChatHistory(prev => [...prev, aiMsg]); - } catch (error) { - console.error(error); - } finally { - setIsGenerating(false); - } - }; - - return { chatHistory, isGenerating, sendMessage }; -} \ No newline at end of file diff --git a/hooks.ts b/hooks.ts index f88eaad..bd05099 100644 --- a/hooks.ts +++ b/hooks.ts @@ -1,417 +1,397 @@ - -import { useState, useCallback, useEffect } from 'react'; -import { UserProfile, BookProject, Chapter, ChatMessage, PlanType, Entity, Idea, WorkflowData } from './types'; -import { INITIAL_CHAPTER } from './constants'; +import { useState, useEffect, useCallback } from 'react'; +import { + BookProject, + Chapter, + Entity, + Idea, + UserProfile, + ChatMessage, + EntityType +} from './types'; +import api from './services/api'; import { generateStoryContent } from './services/geminiService'; -import { authService, dataService } from './services/api'; - -// --- UTILS --- -const safeJSON = (data: any, fallback: any = {}) => { - if (typeof data === 'object' && data !== null) return data; - try { - return data ? JSON.parse(data) : fallback; - } catch (e) { - console.error("JSON Parse Error:", e); - return fallback; - } -}; +import { + DEFAULT_BOOK_TITLE, + DEFAULT_AUTHOR, + INITIAL_CHAPTER +} from './constants'; // --- AUTH HOOK --- -export function useAuth() { +export const useAuth = () => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); - const checkSession = useCallback(async (injectedUser: any = null) => { - setLoading(true); - console.log("[useAuth] Starting session check..."); - - try { - // 1. Get Session from Auth API - const sessionUser = injectedUser || await authService.getSession(); - - if (sessionUser) { - console.log("[useAuth] Session valid:", sessionUser.id); - - // 2. Get Profile from Data API - let profile = await dataService.getProfile(sessionUser.id); - - // 3. Create Profile if missing - if (!profile) { - console.log("[useAuth] Profile missing, creating default..."); - const createResult = await dataService.createItem('profiles', { - user_id: sessionUser.id, - email: sessionUser.email, - full_name: sessionUser.name || 'Author', - ai_actions_limit: 10, - projects_limit: 1, - subscription_plan: 'free', - theme: 'light' + // Check session on mount + useEffect(() => { + const checkSession = async () => { + try { + const session = await api.auth.getSession(); + if (session && session.user) { + // Normalize user data from session + setUser({ + id: session.user.id, + email: session.user.email, + name: session.user.name || 'User', + subscription: { plan: 'free', startDate: Date.now(), status: 'active' }, + usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 }, + preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' }, + stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 } }); - - // Optimistic profile for immediate UI render - profile = { - user_id: sessionUser.id, - email: sessionUser.email, - full_name: sessionUser.name, - ai_actions_limit: 10, - projects_limit: 1, - subscription_plan: 'free' - }; } + } catch (err) { + console.error('Session check failed', err); + } finally { + setLoading(false); + } + }; + checkSession(); + }, []); - // 4. Map DB Profile to Frontend Type - const userProfile: UserProfile = { - id: sessionUser.id, - email: sessionUser.email, - name: profile.full_name || sessionUser.name || 'Writer', - avatar: profile.avatar_url || `https://i.pravatar.cc/150?u=${sessionUser.email}`, - bio: profile.bio || "", - subscription: { - plan: (profile.subscription_plan as PlanType) || 'free', - startDate: 0, - status: 'active' - }, - usage: { - aiActionsCurrent: profile.ai_actions_current || 0, - aiActionsLimit: profile.ai_actions_limit || 10, - projectsLimit: profile.projects_limit || 1 - }, - preferences: { - theme: profile.theme || 'light', - dailyWordGoal: profile.daily_word_goal || 500, - language: profile.language || 'fr' - }, - stats: { - totalWordsWritten: profile.total_words_written || 0, - writingStreak: profile.writing_streak || 0, - lastWriteDate: profile.last_write_date ? new Date(profile.last_write_date).getTime() : 0 - } - }; - - setUser(userProfile); - console.log("[useAuth] User fully loaded."); - - } else { - console.log("[useAuth] No active session."); - setUser(null); + const login = async (email: string, pass: string) => { + setLoading(true); + try { + await api.auth.signIn(email, pass); + // Re-fetch session to get full user details + const session = await api.auth.getSession(); + if (session?.user) { + setUser({ + id: session.user.id, + email: session.user.email, + name: session.user.name || 'User', + subscription: { plan: 'free', startDate: Date.now(), status: 'active' }, + usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 }, + preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' }, + stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 } + }); } } catch (err) { - console.error("[useAuth] Error:", err); - setUser(null); + console.error('Login failed', err); + throw err; } finally { setLoading(false); } - }, []); - - useEffect(() => { checkSession(); }, [checkSession]); - - const login = async (data: any) => { - setLoading(true); - const result = await authService.signIn(data.email, data.password); - - if (result && !result.error) { - // Optimistic session check with returned user data - const userObj = result.user || (result.id ? result : null); - await checkSession(userObj); - } else { - setLoading(false); - } - return result; }; - const signup = async (data: any) => { + const signup = async (email: string, pass: string, name: string) => { setLoading(true); - const result = await authService.signUp(data.email, data.password, data.name); + try { + await api.auth.signUp(email, pass, name); - if (result && !result.error) { - const userObj = result.user || result; - await checkSession(userObj); - } else { + // 1. Try to get session immediately (some backends auto-login) + let session = await api.auth.getSession(); + + // 2. If no session, force login + if (!session?.user) { + await api.auth.signIn(email, pass); + session = await api.auth.getSession(); + } + + if (session?.user) { + setUser({ + id: session.user.id, + email: session.user.email, + name: session.user.name || name, + subscription: { plan: 'free', startDate: Date.now(), status: 'active' }, + usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 }, + preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' }, + stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 } + }); + } + } catch (err) { + console.error('Signup failed', err); + throw err; + } finally { setLoading(false); } - return result; }; const logout = async () => { - setLoading(true); - await authService.signOut(); - setUser(null); - setLoading(false); + try { + await api.auth.signOut(); + setUser(null); + } catch (err) { + console.error('Logout failed', err); + } }; - const incrementUsage = async () => { - // In a real app with RLS, this should be done via a secure backend function - // or by the AI service itself. - // For now we optimistically update UI. + const incrementUsage = () => { if (user) { - setUser(prev => prev ? { - ...prev, - usage: { ...prev.usage, aiActionsCurrent: prev.usage.aiActionsCurrent + 1 } - } : null); + // Optimistic update + setUser({ + ...user, + usage: { ...user.usage, aiActionsCurrent: user.usage.aiActionsCurrent + 1 } + }); + // TODO: Persist usage to backend } }; return { user, login, signup, logout, incrementUsage, loading }; -} +}; // --- PROJECTS HOOK --- -export function useProjects(user: UserProfile | null) { +export const useProjects = (user: UserProfile | null) => { const [projects, setProjects] = useState([]); const [currentProjectId, setCurrentProjectId] = useState(null); const [loading, setLoading] = useState(false); - // Initial Fetch - const fetchProjects = useCallback(async () => { - if (!user) return; - setLoading(true); - try { - const dbProjects = await dataService.getProjects(user.id); + // Load Projects + useEffect(() => { + if (!user) { + setProjects([]); + return; + } + const loadProjects = async () => { + setLoading(true); + try { + const data = await api.data.list('projects'); + // Map DB response to BookProject type + const mapped: BookProject[] = data.map(p => ({ + id: p.id, + title: p.title, + author: p.author, + lastModified: p._created_at || Date.now(), // Fallback + chapters: [], // Loaded on demand usually, but needed for type + entities: [], + ideas: [], + settings: p.settings ? JSON.parse(p.settings) : undefined + })); + setProjects(mapped); + } catch (err) { + console.error('Failed to load projects', err); + } finally { + setLoading(false); + } + }; + loadProjects(); + }, [user]); - const mappedProjects = dbProjects.map((p: any) => ({ - id: p.id.toString(), - title: p.title, - author: p.author || user.name, - lastModified: Date.now(), // No proper modify date in schema yet - settings: { - genre: p.genre, - subGenre: p.sub_genre, - targetAudience: p.target_audience, - tone: p.tone, - pov: p.pov, - tense: p.tense, - synopsis: p.synopsis, - themes: p.themes - }, - styleGuide: p.style_guide, - // Empty init, full load happens on selection + // Load details when project is selected + useEffect(() => { + if (!currentProjectId) return; + + const loadProjectDetails = async () => { + try { + // This fetches everything. In a real app we might optimize. + const fullProject = await api.data.getFullProject(currentProjectId); + setProjects(prev => prev.map(p => p.id === currentProjectId ? fullProject : p)); + } catch (err) { + console.error("Failed to load project details", err); + } + }; + loadProjectDetails(); + }, [currentProjectId]); + + const createProject = async () => { + if (!user) return; + const newProjectData = { + title: DEFAULT_BOOK_TITLE, + author: user.name || DEFAULT_AUTHOR, + settings: JSON.stringify({ genre: 'Fantasy', targetAudience: 'Adult', tone: 'Epic' }) // Defaults + }; + + try { + const created = await api.data.create('projects', newProjectData); + const newProject: BookProject = { + id: created.id.toString(), // Ensure string if needed, DB returns int + title: newProjectData.title, + author: newProjectData.author, + lastModified: Date.now(), chapters: [], entities: [], ideas: [], - workflow: { nodes: [], connections: [] } - })); + settings: JSON.parse(newProjectData.settings) + }; - setProjects(mappedProjects); - } catch (e) { - console.error("[useProjects] Fetch error:", e); - } finally { - setLoading(false); + setProjects(prev => [...prev, newProject]); + + // Create initial chapter + await addChapter(created.id.toString(), INITIAL_CHAPTER); + + return created.id.toString(); + } catch (err) { + console.error('Failed to create project', err); } - }, [user]); + }; - useEffect(() => { fetchProjects(); }, [fetchProjects]); + const updateProject = async (id: string, data: Partial) => { + // Optimistic update + setProjects(prev => prev.map(p => p.id === id ? { ...p, ...data } : p)); - // Deep Fetch Triggered on Project Selection - const fetchProjectDetails = async (projectId: string) => { - const id = parseInt(projectId); + // DB Update try { - const [chapters, entities, ideas, workflows] = await Promise.all([ - dataService.getRelatedData('chapters', id), - dataService.getRelatedData('entities', id), - dataService.getRelatedData('ideas', id), - dataService.getRelatedData('workflows', id) - ]); + const payload: any = {}; + if (data.title) payload.title = data.title; + if (data.author) payload.author = data.author; + if (data.settings) payload.settings = JSON.stringify(data.settings); - setProjects(prev => prev.map(p => p.id === projectId ? { - ...p, - chapters: chapters.map((c: any) => ({ - ...c, - id: c.id.toString() - })), - entities: entities.map((e: any) => ({ - ...e, - id: e.id.toString(), - // JSON Columns parsing - customValues: safeJSON(e.custom_values), - attributes: safeJSON(e.attributes), - storyContext: e.story_context - })), - ideas: ideas.map((i: any) => ({ - ...i, - id: i.id.toString() - })), - workflow: workflows[0] ? { - nodes: safeJSON(workflows[0].nodes, []), - connections: safeJSON(workflows[0].connections, []) - } : { nodes: [], connections: [] } - } : p)); - } catch (e) { - console.error("[useProjects] Details error:", e); + await api.data.update('projects', id, payload); + } catch (err) { + console.error("Failed to update project", err); + // Revert? } }; - // --- CRUD OPERATIONS --- - - const createProject = async () => { - if (!user) return null; - const result = await dataService.createProject({ - user_id: user.id, - title: "Nouveau Roman", - author: user.name, - // Default values - genre: "Fiction", - pov: "First Person", - tense: "Past" - }); - - if (result.status === 'success') { - await fetchProjects(); // Refresh list - return result.id.toString(); - } - return null; - }; - - const updateProject = async (updates: Partial) => { - if (!currentProjectId) return; - const id = parseInt(currentProjectId); - - // Map Frontend updates to DB Columns - const ncbData: any = {}; - if (updates.title) ncbData.title = updates.title; - if (updates.author) ncbData.author = updates.author; - if (updates.settings) { - ncbData.genre = updates.settings.genre; - ncbData.sub_genre = updates.settings.subGenre; - ncbData.synopsis = updates.settings.synopsis; - ncbData.tone = updates.settings.tone; - ncbData.pov = updates.settings.pov; - ncbData.tense = updates.settings.tense; - ncbData.target_audience = updates.settings.targetAudience; - } - if (updates.styleGuide) ncbData.style_guide = updates.styleGuide; - - if (Object.keys(ncbData).length > 0) { - await dataService.updateItem('projects', id, ncbData); - } - - // Optimistic Update - setProjects(prev => prev.map(p => p.id === currentProjectId ? { ...p, ...updates } as BookProject : p)); - }; - - - const addChapter = async () => { - console.log("[Hooks] addChapter called. Current Project ID:", currentProjectId); - if (!currentProjectId) { - console.error("[Hooks] addChapter failed: No currentProjectId"); - alert("Erreur: Impossible d'ajouter un chapitre car aucun projet n'est actif."); - return null; - } - const projectId = parseInt(currentProjectId); - console.log("[Hooks] Creating chapter for project:", projectId); - + const addChapter = async (projectId: string, chapterData: Partial) => { try { - const result = await dataService.createItem('chapters', { + const chapterPayload = { project_id: projectId, - title: "Nouveau Chapitre", - content: "

Contenu...

", - summary: "" - }); - console.log("[Hooks] createItem result:", result); + title: chapterData.title || 'New Chapter', + content: chapterData.content || '', + summary: chapterData.summary || '', + order_index: 0 + }; - if (result.status === 'success') { - await fetchProjectDetails(currentProjectId); - return result.id.toString(); - } else { - console.error("[Hooks] createItem failed status:", result); - } - } catch (e) { - console.error("[Hooks] createItem exception:", e); + const newChap = await api.data.create('chapters', chapterPayload); + + setProjects(prev => prev.map(p => { + if (p.id !== projectId) return p; + return { + ...p, + chapters: [...p.chapters, { + id: newChap.id.toString(), + title: chapterPayload.title, + content: chapterPayload.content, + summary: chapterPayload.summary + }] + }; + })); + } catch (err) { + console.error("Failed to add chapter", err); } - return null; }; - const updateChapter = async (chapterId: string, updates: Partial) => { - if (!currentProjectId) return; - await dataService.updateItem('chapters', parseInt(chapterId), updates); - - setProjects(prev => prev.map(p => p.id === currentProjectId ? { - ...p, - chapters: p.chapters.map(c => c.id === chapterId ? { ...c, ...updates } : c) - } : p)); - }; - - const createEntity = async (entity: Omit) => { - if (!currentProjectId) return null; - const dbPayload = { - project_id: parseInt(currentProjectId), - type: entity.type, - name: entity.name, - description: entity.description, - details: entity.details, - story_context: entity.storyContext, - // Handled as strings in JSON columns - attributes: JSON.stringify(entity.attributes || {}), - custom_values: JSON.stringify(entity.customValues || {}) - }; - - const result = await dataService.createItem('entities', dbPayload); - if (result.status === 'success') { - const newId = result.id.toString(); - // Update local - setProjects(prev => prev.map(p => p.id === currentProjectId ? { + const updateChapter = async (projectId: string, chapterId: string, data: Partial) => { + // Optimistic + setProjects(prev => prev.map(p => { + if (p.id !== projectId) return p; + return { ...p, - entities: [...p.entities, { ...entity, id: newId }] - } : p)); - return newId; + chapters: p.chapters.map(c => c.id === chapterId ? { ...c, ...data } : c) + }; + })); + + try { + await api.data.update('chapters', chapterId, data); + } catch (err) { + console.error("Failed to update chapter", err); } - return null; }; - const updateEntity = async (entityId: string, updates: Partial) => { - if (!currentProjectId) return; - const dbPayload: any = {}; - // Map specific fields - if (updates.name) dbPayload.name = updates.name; - if (updates.description) dbPayload.description = updates.description; - if (updates.details) dbPayload.details = updates.details; - if (updates.storyContext) dbPayload.story_context = updates.storyContext; - if (updates.attributes) dbPayload.attributes = JSON.stringify(updates.attributes); - if (updates.customValues) dbPayload.custom_values = JSON.stringify(updates.customValues); + const createEntity = async (projectId: string, type: EntityType) => { + try { + const entityPayload = { + project_id: projectId, + type: type, + name: `Nouveau ${type}`, + description: '', + details: '', + attributes: '{}' + }; - await dataService.updateItem('entities', parseInt(entityId), dbPayload); + const newEntity = await api.data.create('entities', entityPayload); - setProjects(prev => prev.map(p => p.id === currentProjectId ? { - ...p, - entities: p.entities.map(e => e.id === entityId ? { ...e, ...updates } : e) - } : p)); + setProjects(prev => prev.map(p => { + if (p.id !== projectId) return p; + return { + ...p, + entities: [...p.entities, { + id: newEntity.id.toString(), + type: entityPayload.type, + name: entityPayload.name, + description: entityPayload.description, + details: entityPayload.details, + attributes: JSON.parse(entityPayload.attributes) + }] + }; + })); + } catch (err) { + console.error("Failed to create entity", err); + } }; - const deleteEntity = async (entityId: string) => { - if (!currentProjectId) return; - await dataService.deleteItem('entities', parseInt(entityId)); - setProjects(prev => prev.map(p => p.id === currentProjectId ? { - ...p, - entities: p.entities.filter(e => e.id !== entityId) - } : p)); + const updateEntity = async (projectId: string, entityId: string, data: Partial) => { + setProjects(prev => prev.map(p => { + if (p.id !== projectId) return p; + return { + ...p, + entities: p.entities.map(e => e.id === entityId ? { ...e, ...data } : e) + }; + })); + + try { + const payload: any = { ...data }; + if (data.attributes) payload.attributes = JSON.stringify(data.attributes); + // Clean up fields that might not match DB columns exactly if needed + await api.data.update('entities', entityId, payload); + } catch (err) { + console.error("Failed to update entity", err); + } + }; + + const deleteEntity = async (projectId: string, entityId: string) => { + setProjects(prev => prev.map(p => { + if (p.id !== projectId) return p; + return { + ...p, + entities: p.entities.filter(e => e.id !== entityId) + }; + })); + + try { + await api.data.delete('entities', entityId); + } catch (err) { + console.error("Failed to delete entity", err); + } }; return { projects, currentProjectId, - setCurrentProjectId: (id: string | null) => { - setCurrentProjectId(id); - if (id) fetchProjectDetails(id); - }, - createProject, updateProject, updateChapter, addChapter, - createEntity, updateEntity, deleteEntity, - loading + setCurrentProjectId, + createProject, + updateProject, + addChapter, + updateChapter, + createEntity, + updateEntity, + deleteEntity }; -} +}; // --- CHAT HOOK --- -export function useChat() { +export const useChat = () => { + // Mock implementation for now, or connect to an AI service const [chatHistory, setChatHistory] = useState([]); const [isGenerating, setIsGenerating] = useState(false); - const sendMessage = async (project: BookProject, chapterId: string, msg: string, user: UserProfile, onUsed: () => void) => { - const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text: msg }; + const sendMessage = async ( + project: BookProject, + context: string, + text: string, + user: UserProfile, + incrementUsage: () => void + ) => { + const userMsg: ChatMessage = { + id: Date.now().toString(), + role: 'user', + text: text + }; setChatHistory(prev => [...prev, userMsg]); setIsGenerating(true); try { - const response = await generateStoryContent(project, chapterId, msg, user, onUsed); + const response = await generateStoryContent( + project, + // If context is 'global', pass empty string as chapterId, or handle appropriately + context === 'global' ? '' : context, + text, + user, + incrementUsage + ); + const aiMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'model', @@ -419,12 +399,16 @@ export function useChat() { responseType: response.type }; setChatHistory(prev => [...prev, aiMsg]); - } catch (error) { - console.error(error); + } catch (err) { + setChatHistory(prev => [...prev, { + id: Date.now().toString(), + role: 'model', + text: "Désolé, une erreur est survenue lors de la génération." + }]); } finally { setIsGenerating(false); } }; return { chatHistory, isGenerating, sendMessage }; -} +}; diff --git a/scripts/debug-project.js b/scripts/debug-project.js new file mode 100644 index 0000000..c1d2b91 --- /dev/null +++ b/scripts/debug-project.js @@ -0,0 +1,79 @@ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// 1. Read .env.local +const envPath = path.join(__dirname, '..', '.env.local'); +const envContent = fs.readFileSync(envPath, 'utf8'); +const instanceMatch = envContent.match(/VITE_NCB_INSTANCE=(.+)/); + +if (!instanceMatch) { + console.error("Could not find VITE_NCB_INSTANCE in .env.local"); + process.exit(1); +} + +const instanceId = instanceMatch[1].trim(); + +console.log('Using Instance:', instanceId); + +const BASE_URL = 'https://app.nocodebackend.com/api'; + +async function run() { + // 2. Login + console.log('Logging in...'); + const loginUrl = `${BASE_URL}/user-auth/sign-in/email?Instance=${instanceId}`; + console.log('Login URL:', loginUrl); + + // Add Origin header to mimic trusted origin + const headers = { + 'Content-Type': 'application/json', + 'X-Database-Instance': instanceId, + 'Origin': 'https://app.nocodebackend.com' + }; + + // Login with the admin credentials from AuthPage or a test user + const loginRes = await fetch(loginUrl, { + method: 'POST', + headers: headers, + body: JSON.stringify({ email: 'streaper2@gmail.com', password: 'Kency1313' }) + }); + + if (!loginRes.ok) { + console.error('Login failed:', await loginRes.text()); + return; + } + + const cookie = loginRes.headers.get('set-cookie'); + console.log('Login successful. Cookie received.'); + + // 3. Create Project + console.log('Creating project...'); + // Correct URL based on Swagger: /api/data/create/ + const createUrl = `${BASE_URL}/data/create/projects?Instance=${instanceId}`; + console.log('Create URL:', createUrl); + + const projectData = { + title: "Debug Project", + author: "Debug Bot", + settings: JSON.stringify({ genre: 'Fantasy' }) + }; + + const createRes = await fetch(createUrl, { + method: 'POST', + headers: { + ...headers, + 'Cookie': cookie, // Pass the session cookie + }, + body: JSON.stringify(projectData) + }); + + console.log('Status:', createRes.status); + const text = await createRes.text(); + console.log('Body:', text); +} + +run(); diff --git a/services/api.backup.ts b/services/api.backup.ts deleted file mode 100644 index 3ab9fed..0000000 --- a/services/api.backup.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { BookProject, Chapter, Entity, Idea, WorkflowData, UserProfile } from '../types'; - -// Configuration NoCodeBackend -const AUTH_API_ROOT = 'https://app.nocodebackend.com/api/user-auth'; -const DATA_API_ROOT = 'https://openapi.nocodebackend.com'; -const INSTANCE_ID = '54770_plumeia_db'; -// WARNING: SECRET_KEY restored for debugging purposes (createItem failed without it). -// Ideally, RLS should allow user creation. If not, this key is needed. -const SECRET_KEY = 'c11b7be982d6f369b71a74a7735caf8c2d25d6b09bb5a6ac52d12d49bff3acf1'; -const mcp_server = "ncb_67f8e246c8e4e18598918212035ba6b309d6cac9a6aec919"; -/** - * Récupère les headers avec gestion du token de session - * @param forceUserToken Si true, on n'utilise pas la SECRET_KEY en fallback (pour l'auth) - */ -const getHeaders = (isAuthAction = false) => { - const token = localStorage.getItem('ncb_session_token'); - - const headers: any = { - 'Content-Type': 'application/json', - 'X-Database-Instance': INSTANCE_ID - }; - - // RLS requires the User Token in the Authorization header - if (token && !isAuthAction) { - headers['Authorization'] = `Bearer ${token}`; - } else { - // FALLBACK: Use Secret Key if no token is available (e.g., initial profile creation) - headers['Authorization'] = `Bearer ${SECRET_KEY}`; - } - - return headers; -}; - -/** - * Service d'authentification NoCodeBackend - */ -export const authService = { - async getSession() { - console.log("[Auth] getSession"); - const token = localStorage.getItem('ncb_session_token'); - if (!token) return null; - console.log("[Auth] getSession token:", token); - - try { - const res = await fetch(`${AUTH_API_ROOT}/get-session`, { - method: 'GET', - headers: getHeaders(), - credentials: 'include' - }); - - const data = await res.json(); - console.log("[Auth] getSession data:", data); - - if (!res.ok || !data) { // Vérifie que data existe - if (res.status === 401) localStorage.removeItem('ncb_session_token'); - return null; - } - - // Utilise le chaînage optionnel ?. - return data?.user || (data?.id ? data : null); - } catch (err) { - console.error("[Auth] Erreur critique getSession:", err); - return null; - } - }, - - async signIn(email: string, password: string) { - console.log("[Auth] Tentative de connexion pour:", email); - try { - const res = await fetch(`${AUTH_API_ROOT}/sign-in/email`, { - method: 'POST', - headers: getHeaders(true), - body: JSON.stringify({ email, password }), - credentials: 'include' - }); - - const data = await res.json(); - console.log("[Auth] signIn: Payload reçu:", data); - - if (!res.ok) { - return { error: data.message || 'Identifiants incorrects' }; - } - - // DEBUG: Trace exact token extraction - console.log("[Auth] Token extraction attempt:", { - 'user.token': data.user?.token, - 'token': data.token, - 'session.token': data.session?.token, - 'auth_token': data.auth_token, - 'accessToken': data.accessToken, - '_token': data._token - }); - - const token = data.user?.token || data.token || data.session?.token || data.auth_token || data.accessToken || data._token; - console.log("[Auth] Final extracted token:", token); - - if (token) { - localStorage.setItem('ncb_session_token', token); - } else { - console.error("[Auth] WARNING: No token found in response!"); - } - - return data; - } catch (err) { - console.error("[Auth] Erreur réseau sign-in:", err); - return { error: 'Erreur réseau lors de la connexion' }; - } - }, - - async signUp(email: string, password: string, name: string) { - try { - const res = await fetch(`${AUTH_API_ROOT}/sign-up/email`, { - method: 'POST', - headers: getHeaders(true), - body: JSON.stringify({ email, password, name }), - credentials: 'include' - }); - - const data = await res.json(); - - if (!res.ok) { - return { error: data.message || "Erreur lors de la création du compte" }; - } - - const token = data.user?.token || data.token || data.session?.token || data.auth_token || data.accessToken; - - if (token) { - localStorage.setItem('ncb_session_token', token); - } - - return data; - } catch (err) { - return { error: "Erreur réseau lors de l'inscription" }; - } - }, - - async signOut() { - try { - await fetch(`${AUTH_API_ROOT}/sign-out`, { - method: 'POST', - headers: getHeaders(true), - body: JSON.stringify({}), - credentials: 'include' - }); - } catch (err) { - console.error("[Auth] Erreur sign-out:", err); - } finally { - localStorage.removeItem('ncb_session_token'); - } - } -}; - -export const dataService = { - async getProfile(userId: string) { - try { - const encodedId = encodeURIComponent(userId); - const url = `${DATA_API_ROOT}/read/profiles?Instance=${INSTANCE_ID}&user_id=${encodedId}`; - const res = await fetch(url, { headers: getHeaders(), credentials: 'include' }); - const json = await res.json(); - - if (!res.ok) { - console.error("[Data] Erreur lecture profil API:", json); - return null; - } - - return json.data?.[0] || null; - } catch (err) { - console.error("[Data] Erreur lecture profil:", err); - return null; - } - }, - - async getProjects(userId: string) { - try { - const encodedId = encodeURIComponent(userId); - const url = `${DATA_API_ROOT}/read/projects?Instance=${INSTANCE_ID}&user_id=${encodedId}`; - const res = await fetch(url, { headers: getHeaders(), credentials: 'include' }); - const json = await res.json(); - return json.data || []; - } catch (err) { - console.error("[Data] Erreur lecture projets:", err); - return []; - } - }, - - async createProject(projectData: any) { - try { - const url = `${DATA_API_ROOT}/create/projects?Instance=${INSTANCE_ID}`; - const res = await fetch(url, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(projectData), - credentials: 'include' - }); - const data = await res.json(); - return data; - } catch (err) { - console.error("[Data] Erreur création projet:", err); - return { status: 'error' }; - } - }, - - async getRelatedData(table: string, projectId: number) { - try { - const url = `${DATA_API_ROOT}/read/${table}?Instance=${INSTANCE_ID}&project_id=${projectId}`; - const res = await fetch(url, { headers: getHeaders(), credentials: 'include' }); - const json = await res.json(); - return json.data || []; - } catch (err) { - console.error(`[Data] Erreur lecture ${table}:`, err); - return []; - } - }, - - async createItem(table: string, data: any) { - try { - console.log(`[Data] Création item dans ${table}...`, data); - const url = `${DATA_API_ROOT}/create/${table}?Instance=${INSTANCE_ID}`; - const res = await fetch(url, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(data), - credentials: 'include' - }); - - const result = await res.json(); - if (!res.ok) { - console.error(`[Data] Erreur API création ${table}:`, result); - } - return result; - } catch (err) { - console.error(`[Data] Erreur réseau création item ${table}:`, err); - return { status: 'error', message: err }; - } - }, - - async updateItem(table: string, id: number, data: any) { - try { - const url = `${DATA_API_ROOT}/update/${table}/${id}?Instance=${INSTANCE_ID}`; - await fetch(url, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(data), - credentials: 'include' - }); - - } catch (err) { - console.error(`[Data] Erreur mise à jour item ${table}:`, err); - } - }, - - async deleteItem(table: string, id: number) { - try { - const url = `${DATA_API_ROOT}/delete/${table}/${id}?Instance=${INSTANCE_ID}`; - await fetch(url, { - method: 'DELETE', - headers: getHeaders(), - credentials: 'include' - }); - } catch (err) { - console.error(`[Data] Erreur suppression item ${table}:`, err); - } - } -}; \ No newline at end of file diff --git a/services/api.bak2.ts b/services/api.bak2.ts deleted file mode 100644 index 6f48f3e..0000000 --- a/services/api.bak2.ts +++ /dev/null @@ -1,233 +0,0 @@ - -import { BookProject, Chapter, Entity, Idea, WorkflowData, UserProfile } from '../types'; - -// --- CONFIGURATION --- -const AUTH_API_ROOT = 'https://app.nocodebackend.com/api/user-auth'; -// Use the logic from the guide: Data API is at app.nocodebackend.com/api/data -const DATA_API_ROOT = 'https://app.nocodebackend.com/api/data'; -const INSTANCE_ID = '54770_plumeia_db'; - -// --- HELPERS --- - -/** - * Retrieves headers with the Session Token from localStorage. - * Used for both Auth (when authenticated) and Data requests. - */ -const getHeaders = () => { - const token = localStorage.getItem('ncb_session_token'); - - const headers: Record = { - 'Content-Type': 'application/json', - 'X-Database-Instance': INSTANCE_ID - }; - - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - - return headers; -}; - -/** - * Extracts and stores the session token from the Auth Response. - * Handles various token field names found in NCB responses. - */ -const handleAuthResponse = async (res: Response) => { - const data = await res.json(); - console.log("[API] Auth Response:", data); - - if (!res.ok) { - throw new Error(data.message || 'Authentication failed'); - } - - // extract token - const token = - data.user?.token || - data.token || - data.session?.token || - data.auth_token || - data.accessToken || - data._token; - - if (token) { - console.log("[API] Token extracted and saved:", token); - localStorage.setItem('ncb_session_token', token); - } else { - console.warn("[API] No token found in successful auth response!"); - } - - return data; -}; - -// --- AUTH SERVICE --- - -export const authService = { - async getSession() { - const token = localStorage.getItem('ncb_session_token'); - if (!token) return null; - - try { - const res = await fetch(`${AUTH_API_ROOT}/get-session?Instance=${INSTANCE_ID}`, { - method: 'GET', - headers: getHeaders() - }); - - if (!res.ok) { - // If 401, token is invalid - if (res.status === 401) { - localStorage.removeItem('ncb_session_token'); - } - return null; - } - - const data = await res.json(); - return data.user || data; - } catch (err) { - console.error("[Auth] getSession error:", err); - return null; - } - }, - - async signIn(email: string, password: string) { - try { - const res = await fetch(`${AUTH_API_ROOT}/sign-in/email?Instance=${INSTANCE_ID}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Database-Instance': INSTANCE_ID - }, - body: JSON.stringify({ email, password }) - }); - - return await handleAuthResponse(res); - } catch (err: any) { - console.error("[Auth] signIn error:", err); - return { error: err.message || 'Connection failed' }; - } - }, - - async signUp(email: string, password: string, name: string) { - try { - const res = await fetch(`${AUTH_API_ROOT}/sign-up/email?Instance=${INSTANCE_ID}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Database-Instance': INSTANCE_ID - }, - body: JSON.stringify({ email, password, name }) - }); - - return await handleAuthResponse(res); - } catch (err: any) { - return { error: err.message || 'Registration failed' }; - } - }, - - async signOut() { - try { - await fetch(`${AUTH_API_ROOT}/sign-out?Instance=${INSTANCE_ID}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); - } catch (err) { - console.error("[Auth] signOut error:", err); - } finally { - localStorage.removeItem('ncb_session_token'); - } - } -}; - -// --- DATA SERVICE --- - -export const dataService = { - async getProfile(userId: string) { - try { - // Use standard read endpoint - const encodedId = encodeURIComponent(userId); - const url = `${DATA_API_ROOT}/read/profiles?Instance=${INSTANCE_ID}&user_id=${encodedId}`; - const res = await fetch(url, { headers: getHeaders() }); - const json = await res.json(); - return json.data?.[0] || null; - } catch (err) { - console.error("[Data] getProfile error:", err); - return null; - } - }, - - async getProjects(userId: string) { - try { - const encodedId = encodeURIComponent(userId); - const url = `${DATA_API_ROOT}/read/projects?Instance=${INSTANCE_ID}&user_id=${encodedId}`; - const res = await fetch(url, { headers: getHeaders() }); - const json = await res.json(); - return json.data || []; - } catch (err) { - console.error("[Data] getProjects error:", err); - return []; - } - }, - - async createProject(projectData: any) { - return this.createItem('projects', projectData); - }, - - async getRelatedData(table: string, projectId: number) { - try { - const url = `${DATA_API_ROOT}/read/${table}?Instance=${INSTANCE_ID}&project_id=${projectId}`; - const res = await fetch(url, { headers: getHeaders() }); - const json = await res.json(); - return json.data || []; - } catch (err) { - console.error(`[Data] getRelatedData ${table} error:`, err); - return []; - } - }, - - async createItem(table: string, data: any) { - try { - console.log(`[Data] Creating item in ${table}...`, data); - const url = `${DATA_API_ROOT}/create/${table}?Instance=${INSTANCE_ID}`; - - const res = await fetch(url, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(data) - }); - - const result = await res.json(); - if (!res.ok) { - console.error(`[Data] Create ${table} failed:`, result); - return { status: 'error', message: result.message || 'Creation failed' }; - } - return { status: 'success', ...result }; - } catch (err) { - console.error(`[Data] Create ${table} network error:`, err); - return { status: 'error', message: err }; - } - }, - - async updateItem(table: string, id: number, data: any) { - try { - const url = `${DATA_API_ROOT}/update/${table}/${id}?Instance=${INSTANCE_ID}`; - await fetch(url, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(data) - }); - } catch (err) { - console.error(`[Data] Update ${table} error:`, err); - } - }, - - async deleteItem(table: string, id: number) { - try { - const url = `${DATA_API_ROOT}/delete/${table}/${id}?Instance=${INSTANCE_ID}`; - await fetch(url, { - method: 'DELETE', - headers: getHeaders() - }); - } catch (err) { - console.error(`[Data] Delete ${table} error:`, err); - } - } -}; \ No newline at end of file diff --git a/services/api.ts b/services/api.ts index 39768b3..d2a6997 100644 --- a/services/api.ts +++ b/services/api.ts @@ -1,401 +1,156 @@ +import { + BookProject, + Chapter, + Entity, + Idea, + UserProfile +} from '../types'; -import { BookProject, Chapter, Entity, Idea, WorkflowData, UserProfile } from '../types'; +const API_BASE_URL = '/api'; // Proxied by Vite -// --- CONFIGURATION --- -const AUTH_API_ROOT = '/api/user-auth'; -const DATA_API_ROOT = '/api/data'; -const INSTANCE_ID = '54770_plumeia_db'; +// --- API CLIENT GENERIC --- -// --- HELPERS --- +const api = { + // Generic fetch with cookies + // Generic fetch with cookies + // Generic fetch with cookies + async request(endpoint: string, options: RequestInit = {}) { + // Append Instance to query params + const instanceId = "54770_plumeia"; // Fallback if env missing in some contexts + const separator = endpoint.includes('?') ? '&' : '?'; + const url = `${API_BASE_URL}${endpoint}${separator}Instance=${instanceId}`; -// --- HELPERS --- + const headers = { + 'Content-Type': 'application/json', + 'X-Database-Instance': instanceId, // Keep header just in case + ...options.headers, + }; -const getHeaders = () => { - /*const token = localStorage.getItem('ncb_session_token'); - const sessionData = localStorage.getItem('ncb_session_data'); - console.log("[API] Token:", token); - console.log("[API] Session Data:", sessionData);*/ - const headers: Record = { - 'Content-Type': 'application/json', - 'X-Database-Instance': INSTANCE_ID, - // 'credentials': 'include' - }; - /* - if (token) { - // Fallback standard auth - //headers['Authorization'] = `Bearer ${token}`; - - // User-requested specific Cookie format - // Note: Browsers typically block manual "Cookie" header setting in fetch. - // This is implemented per user request, but relies on the environment allowing it - // or using credentials: 'include' for actual cookies. - let cookieString = `better-auth.session_token=${token}`; - if (sessionData) { - cookieString += `; better-auth.session_data=${sessionData}`; + const response = await fetch(url, { + ...options, + headers, + credentials: 'include', // IMPORTANT: Send cookies + }); + + if (!response.ok) { + let errorMsg = `Error ${response.status}: ${response.statusText}`; + try { + const errorJson = await response.json(); + console.error("API Error JSON:", errorJson); + if (errorJson.error) errorMsg = errorJson.error; + if (errorJson.message) errorMsg = errorJson.message; + } catch (e) { + // Ignore json parse error } - headers['Cookie'] = cookieString; - - console.log("[API] Cookie:", cookieString); + throw new Error(errorMsg); } - - // Debug headers - console.log("[API] Generated Headers:", headers); - */ - return headers; -}; -/* -const handleAuthResponse = async (res: Response) => { - const data = await res.json(); - console.log("[API] Auth Response:", data); - if (!res.ok) { - throw new Error(data.message || 'Authentication failed'); - } + // Return null if 204 No Content + if (response.status === 204) return null; - // Token extraction strategy - const token = - data.user?.token || - data.token || - data.session?.token || - data.auth_token || - data.accessToken || - data._token; - - // Extract session data if available (often needed for Better Auth) - const sessionData = data.session_data || data.session?.data || data.user?.session_data; - - if (token) { - console.log("[API] Token extracted and saved:", token); - localStorage.setItem('ncb_session_token', token); - - if (sessionData) { - console.log("[API] Session Data extracted and saved"); - localStorage.setItem('ncb_session_data', sessionData); - } - } else { - console.warn("[API] No token found in successful auth response!"); - } - - return data; -};*/ - -const handleAuthResponse = async (res: Response) => { - const data = await res.json(); - console.log("[API] Raw Response Data:", data); - - if (!res.ok) { - throw new Error(data.message || 'Authentication failed'); - } - - // --- LOGIQUE DES SETTEURS --- - - // 1. Extraction du Token (selon la structure Better-Auth) - const token = data.session?.token || data.token; - - // 2. Extraction du Session Data - // On prend l'objet session complet et on le stringifie pour le stockage - const sessionData = data.session ? JSON.stringify(data.session) : null; - - if (token) { - localStorage.setItem('ncb_session_token', token); - console.log("[Auth] Token saved to LocalStorage"); - } - - if (sessionData) { - localStorage.setItem('ncb_session_data', sessionData); - console.log("[Auth] Session Data saved to LocalStorage"); - } - - return data; -}; - -// --- AUTH SERVICE --- - -export const authService = { - /*async getSession() { - const token = localStorage.getItem('ncb_session_token'); - // Note: Even if we use cookies, we keep token logic as fallback or for UI state - // if (!token) return null; - - try { - const url = `${AUTH_API_ROOT}/get-session?Instance=${INSTANCE_ID}`; - console.log(`[API] GET session ${url}`); - - const res = await fetch(url, { - method: 'GET', - //headers: getHeaders(), - credentials: 'include' // IMPORTANT: Send cookies - }); - - if (!res.ok) { - console.log(`[API] getSession failed with status: ${res.status}`); - if (res.status === 401) { - localStorage.removeItem('ncb_session_token'); - } - return null; - } - - const data = await res.json(); - console.log("[API] getSession success:", data); - return data.user || data; - } catch (err) { - console.error("[Auth] getSession error:", err); - return null; - } - },*/ - - async getSession() { - console.log("[getSession] démarrage"); - const token = localStorage.getItem('ncb_session_token'); - try { - const url = `${AUTH_API_ROOT}/get-session?Instance=${INSTANCE_ID}`; - const headers: Record = { - 'Content-Type': 'application/json', - 'X-Database-Instance': INSTANCE_ID - }; - - // Si on a un token mais pas encore la session complète, on l'envoie - if (token) { - headers['Cookie'] = `better-auth.session_token=${token}`; - // On peut aussi essayer le header Authorization au cas où - headers['Authorization'] = `Bearer ${token}`; - } - - const res = await fetch(url, { - method: 'GET', - headers: headers, - credentials: 'include' - }); - - if (res.ok) { - const data = await res.json(); - console.log("[getSession] getSession success:", data); - // --- LES SETTEURS ICI --- - if (data.session) { - localStorage.setItem('ncb_session_token', data.session.token); - localStorage.setItem('ncb_session_data', JSON.stringify(data.session)); - console.log("[getSession] Données récupérées depuis getSession"); - } - - return data.user || data; - } - return null; - } catch (err) { - console.error("[Auth] getSession error:", err); - return null; - } + return response.json(); }, + // --- AUTH ENDPOINTS --- + auth: { + async getSession() { + try { + return await api.request('/user-auth/get-session'); + } catch (e) { + return null; // No session + } + }, - async signIn(email: string, password: string) { - try { - // Force X-Database-Instance header as URL param is insufficient for some NCB versions - const url = `${AUTH_API_ROOT}/sign-in/email?Instance=${INSTANCE_ID}`; - console.log(`[API] POST ${url}`, { email, password: '***' }); - - const res = await fetch(url, { + async signIn(email: string, password: string) { + return api.request('/user-auth/sign-in/email', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Database-Instance': INSTANCE_ID - }, body: JSON.stringify({ email, password }), - credentials: 'include' // IMPORTANT: Receive & Save cookies }); - const authData = await handleAuthResponse(res); - await this.getSession(); - console.log("sign in", res); + }, - return await authData; - } catch (err: any) { - console.error("[Auth] signIn error:", err); - return { error: err.message || 'Connection failed' }; - } - }, - - async signUp(email: string, password: string, name: string) { - try { - const url = `${AUTH_API_ROOT}/sign-up/email?Instance=${INSTANCE_ID}`; - console.log(`[API] POST ${url}`, { email, name }); - - const res = await fetch(url, { + async signUp(email: string, password: string, name: string) { + return api.request('/user-auth/sign-up/email', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Database-Instance': INSTANCE_ID - }, body: JSON.stringify({ email, password, name }), - credentials: 'include' // IMPORTANT: Receive & Save cookies }); + }, - return await handleAuthResponse(res); - } catch (err: any) { - console.error("[Auth] signUp error:", err); - return { error: err.message || 'Registration failed' }; - } - }, - - async signOut() { - try { - const url = `${AUTH_API_ROOT}/sign-out?Instance=${INSTANCE_ID}`; - console.log(`[API] POST ${url}`); - - await fetch(url, { + async signOut() { + return api.request('/user-auth/sign-out', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include' // IMPORTANT: Clear cookies }); - } catch (err) { - console.error("[Auth] signOut error:", err); - } finally { - localStorage.removeItem('ncb_session_token'); - } - } -}; - -// --- DATA SERVICE --- - -export const dataService = { - - // -- PROFILES -- - async getProfile(userId: string) { - try { - const encodedId = encodeURIComponent(userId); - const url = `${DATA_API_ROOT}/read/profiles?Instance=${INSTANCE_ID}&user_id=${encodedId}`; - console.log(`[API] GET ${url}`); - - const res = await fetch(url, { - headers: getHeaders(), - credentials: 'include' - }); - const json = await res.json(); - console.log("[Data] getProfile result:", json); - return json.data?.[0] || null; - } catch (err) { - console.error("[Data] getProfile error:", err); - return null; } }, - async createProfile(profileData: any) { - return this.createItem('profiles', profileData); - }, + // --- DATA ENDPOINTS --- + data: { + // Generic list (read all) + async list(table: string) { + // Swagger: GET /read/{table} -> { status: "success", data: [...] } + const res = await api.request<{ data: T[] }>(`/data/read/${table}`); + return res.data || []; + }, - // -- PROJECTS -- - async getProjects(userId: string) { - try { - const encodedId = encodeURIComponent(userId); - const url = `${DATA_API_ROOT}/read/projects?Instance=${INSTANCE_ID}&user_id=${encodedId}`; - console.log(`[API] GET ${url}`); + // Generic get (read one) + async get(table: string, id: string) { + // Swagger: GET /read/{table}/{id} -> { status: "success", data: { ... } } + const res = await api.request<{ data: T }>(`/data/read/${table}/${id}`); + return res.data; + }, - const res = await fetch(url, { - headers: getHeaders(), - credentials: 'include' - }); - const json = await res.json(); - console.log(`[Data] getProjects found ${json.data?.length || 0} items`); - return json.data || []; - } catch (err) { - console.error("[Data] getProjects error:", err); - return []; - } - }, - - async createProject(projectData: Partial & { user_id: string }) { - // Map Frontend types to Database Columns explicitly if needed, but names match mostly - // DB: title, author, genre, sub_genre, target_audience, tone, pov, tense, synopsis, themes, style_guide - return this.createItem('projects', projectData); - }, - - // -- GENERIC CRUD -- - - async getRelatedData(table: string, projectId: number) { - try { - const url = `${DATA_API_ROOT}/read/${table}?Instance=${INSTANCE_ID}&project_id=${projectId}`; - console.log(`[API] GET ${url}`); - - const res = await fetch(url, { - headers: getHeaders(), - credentials: 'include' - }); - const json = await res.json(); - console.log(`[Data] getRelatedData ${table} found ${json.data?.length || 0} items`); - return json.data || []; - } catch (err) { - console.error(`[Data] getRelatedData ${table} error:`, err); - return []; - } - }, - - async createItem(table: string, data: any) { - try { - console.log(`[Data] Creating item in ${table}...`, data); - const url = `${DATA_API_ROOT}/create/${table}?Instance=${INSTANCE_ID}`; - console.log(`[API] POST ${url}`, data); - - const res = await fetch(url, { + // Generic create + async create(table: string, data: any) { + // Swagger: POST /create/{table} -> { status: "success", id: 123 } + // Return the whole response so caller can get ID + return api.request(`/data/create/${table}`, { method: 'POST', - headers: getHeaders(), body: JSON.stringify(data), - credentials: 'include' - }); + }) as Promise; + }, - const result = await res.json(); - console.log(`[Data] Create ${table} response:`, result); - - if (!res.ok) { - console.error(`[Data] Create ${table} failed:`, result); - return { status: 'error', message: result.message || 'Creation failed' }; - } - return { status: 'success', ...result }; - } catch (err) { - console.error(`[Data] Create ${table} network error:`, err); - return { status: 'error', message: err }; - } - }, - - async updateItem(table: string, id: number, data: any) { - try { - const url = `${DATA_API_ROOT}/update/${table}/${id}?Instance=${INSTANCE_ID}`; - console.log(`[API] PUT ${url}`, data); - - const res = await fetch(url, { + // Generic update + async update(table: string, id: string, data: any) { + // Swagger: PUT /update/{table}/{id} + // Note: Swagger for update usually expects "update" path prefix + return api.request(`/data/update/${table}/${id}`, { method: 'PUT', - headers: getHeaders(), body: JSON.stringify(data), - credentials: 'include' - }); + }) as Promise; + }, - if (!res.ok) { - const err = await res.json(); - console.error(`[Data] Update ${table} failed:`, err); - } else { - console.log(`[Data] Update ${table} success`); - } - } catch (err) { - console.error(`[Data] Update ${table} error:`, err); - } - }, - - async deleteItem(table: string, id: number) { - try { - const url = `${DATA_API_ROOT}/delete/${table}/${id}?Instance=${INSTANCE_ID}`; - console.log(`[API] DELETE ${url}`); - - const res = await fetch(url, { + // Generic delete + async delete(table: string, id: string) { + // Swagger: DELETE /delete/{table}/{id} + return api.request(`/data/delete/${table}/${id}`, { method: 'DELETE', - headers: getHeaders(), - credentials: 'include' }); + }, - if (!res.ok) { - const err = await res.json(); - console.error(`[Data] Delete ${table} failed:`, err); - } else { - console.log(`[Data] Delete ${table} success`); - } - } catch (err) { - console.error(`[Data] Delete ${table} error:`, err); + // --- Specialized Project Methods --- + + // Fetch full project with related data (chapters, entities, etc) + // NOTE: In a real app, you might want to fetch these separately or use a join if supported. + // For now, we'll fetch the project and then fetch its children. + async getFullProject(projectId: string): Promise { + const project = await this.get('projects', projectId); + + // Fetch related data in parallel + const [chapters, entities, ideas] = await Promise.all([ + api.request(`/data/chapters/list?project_id=${projectId}`), + api.request(`/data/entities/list?project_id=${projectId}`), + api.request(`/data/ideas/list?project_id=${projectId}`), + ]); + + return { + ...project, + chapters: chapters || [], + entities: entities || [], + ideas: ideas || [], + // templates: [], // Not yet in DB + // workflow: null // Not yet in DB + }; } } }; + +export default api; diff --git a/vite.config.ts b/vite.config.ts index c60bf67..96a0b76 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,12 +15,40 @@ export default defineConfig(({ mode }) => { changeOrigin: true, secure: false, cookieDomainRewrite: "localhost", + headers: { + "Origin": "https://app.nocodebackend.com" // Bypass CORS/CSRF checks + }, + configure: (proxy, _options) => { + proxy.on('proxyRes', (proxyRes, req, _res) => { + if (proxyRes.headers['set-cookie']) { + proxyRes.headers['set-cookie'] = proxyRes.headers['set-cookie'].map((cookie) => { + return cookie + .replace(/; secure/gi, '') // Remove Secure flag + .replace(/; samesite=none/gi, '; SameSite=Lax'); // Fix SameSite for localhost + }); + } + }); + }, }, '/api/data': { target: 'https://app.nocodebackend.com', changeOrigin: true, secure: false, cookieDomainRewrite: "localhost", + headers: { + "Origin": "https://app.nocodebackend.com" // Bypass CORS/CSRF checks + }, + configure: (proxy, _options) => { + proxy.on('proxyRes', (proxyRes, req, _res) => { + if (proxyRes.headers['set-cookie']) { + proxyRes.headers['set-cookie'] = proxyRes.headers['set-cookie'].map((cookie) => { + return cookie + .replace(/; secure/gi, '') // Remove Secure flag + .replace(/; samesite=none/gi, '; SameSite=Lax'); // Fix SameSite for localhost + }); + } + }); + }, } } },