login ok
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, 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
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
477
services/api.ts
477
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 ---
|
// --- 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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user