This commit is contained in:
2026-02-16 22:02:45 +01:00
parent af9d69cc78
commit 85a585131e
9 changed files with 551 additions and 1593 deletions

View File

@@ -31,7 +31,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
setError(''); setError('');
try { try {
const result = await login(adminData); const result = await login(adminData.email, adminData.password);
if (result?.error) setError(result.error); if (result?.error) setError(result.error);
} catch (e) { } catch (e) {
setError('Erreur de connexion au service.'); setError('Erreur de connexion au service.');
@@ -48,9 +48,9 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
try { try {
let result; let result;
if (mode === 'signup') { if (mode === 'signup') {
result = await signup(formData); result = await signup(formData.email, formData.password, formData.name);
} else { } else {
result = await login({ email: formData.email, password: formData.password }); result = await login(formData.email, formData.password);
} }
if (result?.error) { if (result?.error) {

View File

@@ -22,16 +22,10 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
setLoading(true); setLoading(true);
try { try {
const result = await login({ email, password }); await login(email, password);
onSuccess();
if (result && result.error) { } catch (err: any) {
setError(result.error); setError(err.message || "Une erreur inattendue est survenue.");
setLoading(false);
} else {
onSuccess();
}
} catch (err) {
setError("Une erreur inattendue est survenue.");
setLoading(false); setLoading(false);
} }
} }

View File

@@ -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<UserProfile | null>(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<BookProject[]>([]);
const [currentProjectId, setCurrentProjectId] = useState<string | null>(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: "<p>Écrivez ici votre prochain chapitre...</p>",
summary: ""
});
if (result.status === 'success' && result.id) {
await fetchProjectDetails(currentProjectId);
return result.id.toString();
}
return null;
};
const createEntity = async (entity: Omit<Entity, 'id'>) => {
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<Entity>) => {
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<BookProject>) => {
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<Chapter>) => {
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<ChatMessage[]>([]);
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 };
}

656
hooks.ts
View File

@@ -1,417 +1,397 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useCallback, useEffect } from 'react'; import {
import { UserProfile, BookProject, Chapter, ChatMessage, PlanType, Entity, Idea, WorkflowData } from './types'; BookProject,
import { INITIAL_CHAPTER } from './constants'; Chapter,
Entity,
Idea,
UserProfile,
ChatMessage,
EntityType
} from './types';
import api from './services/api';
import { generateStoryContent } from './services/geminiService'; import { generateStoryContent } from './services/geminiService';
import { authService, dataService } from './services/api'; import {
DEFAULT_BOOK_TITLE,
// --- UTILS --- DEFAULT_AUTHOR,
const safeJSON = (data: any, fallback: any = {}) => { INITIAL_CHAPTER
if (typeof data === 'object' && data !== null) return data; } from './constants';
try {
return data ? JSON.parse(data) : fallback;
} catch (e) {
console.error("JSON Parse Error:", e);
return fallback;
}
};
// --- AUTH HOOK --- // --- AUTH HOOK ---
export function useAuth() { export const useAuth = () => {
const [user, setUser] = useState<UserProfile | null>(null); const [user, setUser] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const checkSession = useCallback(async (injectedUser: any = null) => { // Check session on mount
setLoading(true); useEffect(() => {
console.log("[useAuth] Starting session check..."); const checkSession = async () => {
try {
try { const session = await api.auth.getSession();
// 1. Get Session from Auth API if (session && session.user) {
const sessionUser = injectedUser || await authService.getSession(); // Normalize user data from session
setUser({
if (sessionUser) { id: session.user.id,
console.log("[useAuth] Session valid:", sessionUser.id); email: session.user.email,
name: session.user.name || 'User',
// 2. Get Profile from Data API subscription: { plan: 'free', startDate: Date.now(), status: 'active' },
let profile = await dataService.getProfile(sessionUser.id); usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 },
preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' },
// 3. Create Profile if missing stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 }
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'
}); });
// 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 login = async (email: string, pass: string) => {
const userProfile: UserProfile = { setLoading(true);
id: sessionUser.id, try {
email: sessionUser.email, await api.auth.signIn(email, pass);
name: profile.full_name || sessionUser.name || 'Writer', // Re-fetch session to get full user details
avatar: profile.avatar_url || `https://i.pravatar.cc/150?u=${sessionUser.email}`, const session = await api.auth.getSession();
bio: profile.bio || "", if (session?.user) {
subscription: { setUser({
plan: (profile.subscription_plan as PlanType) || 'free', id: session.user.id,
startDate: 0, email: session.user.email,
status: 'active' name: session.user.name || 'User',
}, subscription: { plan: 'free', startDate: Date.now(), status: 'active' },
usage: { usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 },
aiActionsCurrent: profile.ai_actions_current || 0, preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' },
aiActionsLimit: profile.ai_actions_limit || 10, stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 }
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);
} }
} catch (err) { } catch (err) {
console.error("[useAuth] Error:", err); console.error('Login failed', err);
setUser(null); throw err;
} finally { } finally {
setLoading(false); 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); 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) { // 1. Try to get session immediately (some backends auto-login)
const userObj = result.user || result; let session = await api.auth.getSession();
await checkSession(userObj);
} else { // 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); setLoading(false);
} }
return result;
}; };
const logout = async () => { const logout = async () => {
setLoading(true); try {
await authService.signOut(); await api.auth.signOut();
setUser(null); setUser(null);
setLoading(false); } catch (err) {
console.error('Logout failed', err);
}
}; };
const incrementUsage = async () => { const incrementUsage = () => {
// 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.
if (user) { if (user) {
setUser(prev => prev ? { // Optimistic update
...prev, setUser({
usage: { ...prev.usage, aiActionsCurrent: prev.usage.aiActionsCurrent + 1 } ...user,
} : null); usage: { ...user.usage, aiActionsCurrent: user.usage.aiActionsCurrent + 1 }
});
// TODO: Persist usage to backend
} }
}; };
return { user, login, signup, logout, incrementUsage, loading }; return { user, login, signup, logout, incrementUsage, loading };
} };
// --- PROJECTS HOOK --- // --- PROJECTS HOOK ---
export function useProjects(user: UserProfile | null) { export const useProjects = (user: UserProfile | null) => {
const [projects, setProjects] = useState<BookProject[]>([]); const [projects, setProjects] = useState<BookProject[]>([]);
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null); const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Initial Fetch // Load Projects
const fetchProjects = useCallback(async () => { useEffect(() => {
if (!user) return; if (!user) {
setLoading(true); setProjects([]);
try { return;
const dbProjects = await dataService.getProjects(user.id); }
const loadProjects = async () => {
setLoading(true);
try {
const data = await api.data.list<any>('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) => ({ // Load details when project is selected
id: p.id.toString(), useEffect(() => {
title: p.title, if (!currentProjectId) return;
author: p.author || user.name,
lastModified: Date.now(), // No proper modify date in schema yet const loadProjectDetails = async () => {
settings: { try {
genre: p.genre, // This fetches everything. In a real app we might optimize.
subGenre: p.sub_genre, const fullProject = await api.data.getFullProject(currentProjectId);
targetAudience: p.target_audience, setProjects(prev => prev.map(p => p.id === currentProjectId ? fullProject : p));
tone: p.tone, } catch (err) {
pov: p.pov, console.error("Failed to load project details", err);
tense: p.tense, }
synopsis: p.synopsis, };
themes: p.themes loadProjectDetails();
}, }, [currentProjectId]);
styleGuide: p.style_guide,
// Empty init, full load happens on selection 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<any>('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: [], chapters: [],
entities: [], entities: [],
ideas: [], ideas: [],
workflow: { nodes: [], connections: [] } settings: JSON.parse(newProjectData.settings)
})); };
setProjects(mappedProjects); setProjects(prev => [...prev, newProject]);
} catch (e) {
console.error("[useProjects] Fetch error:", e); // Create initial chapter
} finally { await addChapter(created.id.toString(), INITIAL_CHAPTER);
setLoading(false);
return created.id.toString();
} catch (err) {
console.error('Failed to create project', err);
} }
}, [user]); };
useEffect(() => { fetchProjects(); }, [fetchProjects]); const updateProject = async (id: string, data: Partial<BookProject>) => {
// Optimistic update
setProjects(prev => prev.map(p => p.id === id ? { ...p, ...data } : p));
// Deep Fetch Triggered on Project Selection // DB Update
const fetchProjectDetails = async (projectId: string) => {
const id = parseInt(projectId);
try { try {
const [chapters, entities, ideas, workflows] = await Promise.all([ const payload: any = {};
dataService.getRelatedData('chapters', id), if (data.title) payload.title = data.title;
dataService.getRelatedData('entities', id), if (data.author) payload.author = data.author;
dataService.getRelatedData('ideas', id), if (data.settings) payload.settings = JSON.stringify(data.settings);
dataService.getRelatedData('workflows', id)
]);
setProjects(prev => prev.map(p => p.id === projectId ? { await api.data.update('projects', id, payload);
...p, } catch (err) {
chapters: chapters.map((c: any) => ({ console.error("Failed to update project", err);
...c, // Revert?
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);
} }
}; };
// --- CRUD OPERATIONS --- const addChapter = async (projectId: string, chapterData: Partial<Chapter>) => {
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<BookProject>) => {
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);
try { try {
const result = await dataService.createItem('chapters', { const chapterPayload = {
project_id: projectId, project_id: projectId,
title: "Nouveau Chapitre", title: chapterData.title || 'New Chapter',
content: "<p>Contenu...</p>", content: chapterData.content || '',
summary: "" summary: chapterData.summary || '',
}); order_index: 0
console.log("[Hooks] createItem result:", result); };
if (result.status === 'success') { const newChap = await api.data.create<any>('chapters', chapterPayload);
await fetchProjectDetails(currentProjectId);
return result.id.toString(); setProjects(prev => prev.map(p => {
} else { if (p.id !== projectId) return p;
console.error("[Hooks] createItem failed status:", result); return {
} ...p,
} catch (e) { chapters: [...p.chapters, {
console.error("[Hooks] createItem exception:", e); 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<Chapter>) => { const updateChapter = async (projectId: string, chapterId: string, data: Partial<Chapter>) => {
if (!currentProjectId) return; // Optimistic
await dataService.updateItem('chapters', parseInt(chapterId), updates); setProjects(prev => prev.map(p => {
if (p.id !== projectId) return p;
setProjects(prev => prev.map(p => p.id === currentProjectId ? { return {
...p,
chapters: p.chapters.map(c => c.id === chapterId ? { ...c, ...updates } : c)
} : p));
};
const createEntity = async (entity: Omit<Entity, 'id'>) => {
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 ? {
...p, ...p,
entities: [...p.entities, { ...entity, id: newId }] chapters: p.chapters.map(c => c.id === chapterId ? { ...c, ...data } : c)
} : p)); };
return newId; }));
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<Entity>) => { const createEntity = async (projectId: string, type: EntityType) => {
if (!currentProjectId) return; try {
const dbPayload: any = {}; const entityPayload = {
// Map specific fields project_id: projectId,
if (updates.name) dbPayload.name = updates.name; type: type,
if (updates.description) dbPayload.description = updates.description; name: `Nouveau ${type}`,
if (updates.details) dbPayload.details = updates.details; description: '',
if (updates.storyContext) dbPayload.story_context = updates.storyContext; details: '',
if (updates.attributes) dbPayload.attributes = JSON.stringify(updates.attributes); attributes: '{}'
if (updates.customValues) dbPayload.custom_values = JSON.stringify(updates.customValues); };
await dataService.updateItem('entities', parseInt(entityId), dbPayload); const newEntity = await api.data.create<any>('entities', entityPayload);
setProjects(prev => prev.map(p => p.id === currentProjectId ? { setProjects(prev => prev.map(p => {
...p, if (p.id !== projectId) return p;
entities: p.entities.map(e => e.id === entityId ? { ...e, ...updates } : e) return {
} : p)); ...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) => { const updateEntity = async (projectId: string, entityId: string, data: Partial<Entity>) => {
if (!currentProjectId) return; setProjects(prev => prev.map(p => {
await dataService.deleteItem('entities', parseInt(entityId)); if (p.id !== projectId) return p;
setProjects(prev => prev.map(p => p.id === currentProjectId ? { return {
...p, ...p,
entities: p.entities.filter(e => e.id !== entityId) entities: p.entities.map(e => e.id === entityId ? { ...e, ...data } : e)
} : p)); };
}));
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 { return {
projects, projects,
currentProjectId, currentProjectId,
setCurrentProjectId: (id: string | null) => { setCurrentProjectId,
setCurrentProjectId(id); createProject,
if (id) fetchProjectDetails(id); updateProject,
}, addChapter,
createProject, updateProject, updateChapter, addChapter, updateChapter,
createEntity, updateEntity, deleteEntity, createEntity,
loading updateEntity,
deleteEntity
}; };
} };
// --- CHAT HOOK --- // --- CHAT HOOK ---
export function useChat() { export const useChat = () => {
// Mock implementation for now, or connect to an AI service
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]); const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
const [isGenerating, setIsGenerating] = useState(false); const [isGenerating, setIsGenerating] = useState(false);
const sendMessage = async (project: BookProject, chapterId: string, msg: string, user: UserProfile, onUsed: () => void) => { const sendMessage = async (
const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text: msg }; 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]); setChatHistory(prev => [...prev, userMsg]);
setIsGenerating(true); setIsGenerating(true);
try { 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 = { const aiMsg: ChatMessage = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
role: 'model', role: 'model',
@@ -419,12 +399,16 @@ export function useChat() {
responseType: response.type responseType: response.type
}; };
setChatHistory(prev => [...prev, aiMsg]); setChatHistory(prev => [...prev, aiMsg]);
} catch (error) { } catch (err) {
console.error(error); setChatHistory(prev => [...prev, {
id: Date.now().toString(),
role: 'model',
text: "Désolé, une erreur est survenue lors de la génération."
}]);
} finally { } finally {
setIsGenerating(false); setIsGenerating(false);
} }
}; };
return { chatHistory, isGenerating, sendMessage }; return { chatHistory, isGenerating, sendMessage };
} };

79
scripts/debug-project.js Normal file
View File

@@ -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/<table>
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();

View File

@@ -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);
}
}
};

View File

@@ -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<string, string> = {
'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);
}
}
};

View File

@@ -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 --- // --- API CLIENT GENERIC ---
const AUTH_API_ROOT = '/api/user-auth';
const DATA_API_ROOT = '/api/data';
const INSTANCE_ID = '54770_plumeia_db';
// --- HELPERS --- const api = {
// Generic fetch with cookies
// Generic fetch with cookies
// Generic fetch with cookies
async request<T = any>(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 response = await fetch(url, {
/*const token = localStorage.getItem('ncb_session_token'); ...options,
const sessionData = localStorage.getItem('ncb_session_data'); headers,
console.log("[API] Token:", token); credentials: 'include', // IMPORTANT: Send cookies
console.log("[API] Session Data:", sessionData);*/ });
const headers: Record<string, string> = {
'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 if (!response.ok) {
// Note: Browsers typically block manual "Cookie" header setting in fetch. let errorMsg = `Error ${response.status}: ${response.statusText}`;
// This is implemented per user request, but relies on the environment allowing it try {
// or using credentials: 'include' for actual cookies. const errorJson = await response.json();
let cookieString = `better-auth.session_token=${token}`; console.error("API Error JSON:", errorJson);
if (sessionData) { if (errorJson.error) errorMsg = errorJson.error;
cookieString += `; better-auth.session_data=${sessionData}`; if (errorJson.message) errorMsg = errorJson.message;
} catch (e) {
// Ignore json parse error
} }
headers['Cookie'] = cookieString; throw new Error(errorMsg);
console.log("[API] Cookie:", cookieString);
} }
// Debug headers // Return null if 204 No Content
console.log("[API] Generated Headers:", headers); if (response.status === 204) return null;
*/
return headers;
};
/*
const handleAuthResponse = async (res: Response) => {
const data = await res.json();
console.log("[API] Auth Response:", data);
if (!res.ok) { return response.json();
throw new Error(data.message || 'Authentication failed');
}
// 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<string, string> = {
'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;
}
}, },
// --- 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) { async signIn(email: string, password: string) {
try { return api.request('/user-auth/sign-in/email', {
// 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, {
method: 'POST', method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Database-Instance': INSTANCE_ID
},
body: JSON.stringify({ email, password }), 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; async signUp(email: string, password: string, name: string) {
} catch (err: any) { return api.request('/user-auth/sign-up/email', {
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, {
method: 'POST', method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Database-Instance': INSTANCE_ID
},
body: JSON.stringify({ email, password, name }), body: JSON.stringify({ email, password, name }),
credentials: 'include' // IMPORTANT: Receive & Save cookies
}); });
},
return await handleAuthResponse(res); async signOut() {
} catch (err: any) { return api.request('/user-auth/sign-out', {
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, {
method: 'POST', 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) { // --- DATA ENDPOINTS ---
return this.createItem('profiles', profileData); data: {
}, // Generic list (read all)
async list<T>(table: string) {
// Swagger: GET /read/{table} -> { status: "success", data: [...] }
const res = await api.request<{ data: T[] }>(`/data/read/${table}`);
return res.data || [];
},
// -- PROJECTS -- // Generic get (read one)
async getProjects(userId: string) { async get<T>(table: string, id: string) {
try { // Swagger: GET /read/{table}/{id} -> { status: "success", data: { ... } }
const encodedId = encodeURIComponent(userId); const res = await api.request<{ data: T }>(`/data/read/${table}/${id}`);
const url = `${DATA_API_ROOT}/read/projects?Instance=${INSTANCE_ID}&user_id=${encodedId}`; return res.data;
console.log(`[API] GET ${url}`); },
const res = await fetch(url, { // Generic create
headers: getHeaders(), async create<T>(table: string, data: any) {
credentials: 'include' // Swagger: POST /create/{table} -> { status: "success", id: 123 }
}); // Return the whole response so caller can get ID
const json = await res.json(); return api.request(`/data/create/${table}`, {
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<BookProject> & { 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, {
method: 'POST', method: 'POST',
headers: getHeaders(),
body: JSON.stringify(data), body: JSON.stringify(data),
credentials: 'include' }) as Promise<T>;
}); },
const result = await res.json(); // Generic update
console.log(`[Data] Create ${table} response:`, result); async update<T>(table: string, id: string, data: any) {
// Swagger: PUT /update/{table}/{id}
if (!res.ok) { // Note: Swagger for update usually expects "update" path prefix
console.error(`[Data] Create ${table} failed:`, result); return api.request(`/data/update/${table}/${id}`, {
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, {
method: 'PUT', method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(data), body: JSON.stringify(data),
credentials: 'include' }) as Promise<T>;
}); },
if (!res.ok) { // Generic delete
const err = await res.json(); async delete(table: string, id: string) {
console.error(`[Data] Update ${table} failed:`, err); // Swagger: DELETE /delete/{table}/{id}
} else { return api.request(`/data/delete/${table}/${id}`, {
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, {
method: 'DELETE', method: 'DELETE',
headers: getHeaders(),
credentials: 'include'
}); });
},
if (!res.ok) { // --- Specialized Project Methods ---
const err = await res.json();
console.error(`[Data] Delete ${table} failed:`, err); // Fetch full project with related data (chapters, entities, etc)
} else { // NOTE: In a real app, you might want to fetch these separately or use a join if supported.
console.log(`[Data] Delete ${table} success`); // For now, we'll fetch the project and then fetch its children.
} async getFullProject(projectId: string): Promise<BookProject> {
} catch (err) { const project = await this.get<any>('projects', projectId);
console.error(`[Data] Delete ${table} error:`, err);
// 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;

View File

@@ -15,12 +15,40 @@ export default defineConfig(({ mode }) => {
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
cookieDomainRewrite: "localhost", 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': { '/api/data': {
target: 'https://app.nocodebackend.com', target: 'https://app.nocodebackend.com',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
cookieDomainRewrite: "localhost", 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
});
}
});
},
} }
} }
}, },