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

View File

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