login ok
This commit is contained in:
@@ -31,7 +31,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ 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<AuthPageProps> = ({ 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) {
|
||||
|
||||
@@ -22,16 +22,10 @@ const LoginPage: React.FC<LoginPageProps> = ({ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
656
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<UserProfile | null>(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<BookProject[]>([]);
|
||||
const [currentProjectId, setCurrentProjectId] = useState<string | null>(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<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) => ({
|
||||
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<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: [],
|
||||
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<BookProject>) => {
|
||||
// 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<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);
|
||||
|
||||
const addChapter = async (projectId: string, chapterData: Partial<Chapter>) => {
|
||||
try {
|
||||
const result = await dataService.createItem('chapters', {
|
||||
const chapterPayload = {
|
||||
project_id: projectId,
|
||||
title: "Nouveau Chapitre",
|
||||
content: "<p>Contenu...</p>",
|
||||
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<any>('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<Chapter>) => {
|
||||
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<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 ? {
|
||||
const updateChapter = async (projectId: string, chapterId: string, data: Partial<Chapter>) => {
|
||||
// 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<Entity>) => {
|
||||
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<any>('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<Entity>) => {
|
||||
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<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 };
|
||||
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 };
|
||||
}
|
||||
};
|
||||
|
||||
79
scripts/debug-project.js
Normal file
79
scripts/debug-project.js
Normal 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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
479
services/api.ts
479
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<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 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<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
|
||||
// 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<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;
|
||||
}
|
||||
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<T>(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<T>(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<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, {
|
||||
// Generic create
|
||||
async create<T>(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<T>;
|
||||
},
|
||||
|
||||
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<T>(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<T>;
|
||||
},
|
||||
|
||||
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<BookProject> {
|
||||
const project = await this.get<any>('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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user