authentification nocodebackend ok
This commit is contained in:
264
services/api.backup.ts
Normal file
264
services/api.backup.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
233
services/api.bak2.ts
Normal file
233
services/api.bak2.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
235
services/api.ts
Normal file
235
services/api.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
|
||||
import { BookProject, Chapter, Entity, Idea, WorkflowData, UserProfile } from '../types';
|
||||
|
||||
// --- CONFIGURATION ---
|
||||
const AUTH_API_ROOT = 'https://app.nocodebackend.com/api/user-auth';
|
||||
const DATA_API_ROOT = 'https://app.nocodebackend.com/api/data';
|
||||
const INSTANCE_ID = '54770_plumeia_db';
|
||||
|
||||
// --- HELPERS ---
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Token extraction strategy based on NCB patterns
|
||||
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 (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 {
|
||||
// Force X-Database-Instance header as URL param is insufficient for some NCB versions
|
||||
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 = {
|
||||
|
||||
// -- PROFILES --
|
||||
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() });
|
||||
const json = await res.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);
|
||||
},
|
||||
|
||||
// -- PROJECTS --
|
||||
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: 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}`;
|
||||
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),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
185
services/geminiService.ts
Normal file
185
services/geminiService.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import { BookProject, Chapter, Entity, UserProfile } from "../types";
|
||||
|
||||
const truncate = (str: string, length: number) => {
|
||||
if (!str) return "";
|
||||
return str.length > length ? str.substring(0, length) + "..." : str;
|
||||
};
|
||||
|
||||
const checkUsage = (user: UserProfile) => {
|
||||
if (user.subscription.plan === 'master') return true;
|
||||
return user.usage.aiActionsCurrent < user.usage.aiActionsLimit;
|
||||
};
|
||||
|
||||
const buildContextPrompt = (project: BookProject, currentChapterId: string, instruction: string) => {
|
||||
const currentChapterIndex = project.chapters.findIndex(c => c.id === currentChapterId);
|
||||
const previousSummaries = project.chapters
|
||||
.slice(0, currentChapterIndex)
|
||||
.map((c, i) => `Chapitre ${i + 1} (${c.title}): ${c.summary || truncate(c.content.replace(/<[^>]*>?/gm, ''), 200)}`)
|
||||
.join('\n');
|
||||
|
||||
const entitiesContext = project.entities
|
||||
.map(e => {
|
||||
const base = `[${e.type}] ${e.name}: ${truncate(e.description, 150)}`;
|
||||
const context = e.storyContext ? `\n - VÉCU/ÉVOLUTION DANS L'HISTOIRE: ${truncate(e.storyContext, 500)}` : '';
|
||||
return base + context;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const ideasContext = (project.ideas || [])
|
||||
.map(i => {
|
||||
const statusMap: Record<string, string> = { todo: 'À FAIRE', progress: 'EN COURS', done: 'TERMINÉ' };
|
||||
return `[IDÉE - ${statusMap[i.status]}] ${i.title}: ${truncate(i.description, 100)}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const currentContent = project.chapters[currentChapterIndex]?.content.replace(/<[^>]*>?/gm, '') || "";
|
||||
const s = project.settings;
|
||||
const settingsPrompt = s ? `
|
||||
PARAMÈTRES DU ROMAN:
|
||||
- Genre: ${s.genre} ${s.subGenre ? `(${s.subGenre})` : ''}
|
||||
- Public: ${s.targetAudience}
|
||||
- Ton: ${s.tone}
|
||||
- Narration: ${s.pov}
|
||||
- Temps: ${s.tense}
|
||||
- Thèmes: ${s.themes}
|
||||
- Synopsis Global: ${truncate(s.synopsis || '', 500)}
|
||||
` : "";
|
||||
|
||||
return `
|
||||
Tu es un assistant éditorial expert et un co-auteur créatif.
|
||||
L'utilisateur écrit un livre intitulé "${project.title}".
|
||||
|
||||
${settingsPrompt}
|
||||
|
||||
CONTEXTE DE L'HISTOIRE (Résumé des chapitres précédents):
|
||||
${previousSummaries || "Aucun chapitre précédent."}
|
||||
|
||||
BIBLE DU MONDE (Personnages et Lieux):
|
||||
${entitiesContext || "Aucune fiche créée."}
|
||||
|
||||
BOÎTE À IDÉES & NOTES (Pistes de l'auteur):
|
||||
${ideasContext || "Aucune note."}
|
||||
|
||||
CHAPITRE ACTUEL (Texte brut):
|
||||
${truncate(currentContent, 3000)}
|
||||
|
||||
STYLE D'ÉCRITURE SPÉCIFIQUE (Instruction de l'auteur):
|
||||
${project.styleGuide || "Standard, neutre."}
|
||||
|
||||
TA MISSION:
|
||||
${instruction}
|
||||
`;
|
||||
};
|
||||
|
||||
export const generateStoryContent = async (
|
||||
project: BookProject,
|
||||
currentChapterId: string,
|
||||
userPrompt: string,
|
||||
user: UserProfile,
|
||||
onSuccess: () => void
|
||||
): Promise<{ text: string, type: 'draft' | 'reflection' }> => {
|
||||
if (!checkUsage(user)) {
|
||||
return { text: "Limite d'actions IA atteinte pour ce mois. Passez au plan Pro !", type: 'reflection' };
|
||||
}
|
||||
|
||||
try {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const finalPrompt = buildContextPrompt(project, currentChapterId, userPrompt);
|
||||
|
||||
// Pro/Master plan users get better models (simulated here)
|
||||
const modelName = user.subscription.plan === 'master' ? 'gemini-3-pro-preview' : 'gemini-3-flash-preview';
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: modelName,
|
||||
contents: finalPrompt,
|
||||
config: {
|
||||
temperature: 0.7,
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
responseType: {
|
||||
type: Type.STRING,
|
||||
enum: ["draft", "reflection"]
|
||||
},
|
||||
content: {
|
||||
type: Type.STRING
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onSuccess();
|
||||
const result = JSON.parse(response.text || "{}");
|
||||
return {
|
||||
text: result.content || "Erreur de génération.",
|
||||
type: result.responseType || "reflection"
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("AI Generation Error:", error);
|
||||
return { text: "Erreur lors de la communication avec l'IA.", type: 'reflection' };
|
||||
}
|
||||
};
|
||||
|
||||
export const updateEntityContexts = async (text: string, entities: Entity[]): Promise<Entity[]> => {
|
||||
if (!text || entities.length === 0) return entities;
|
||||
try {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const entityList = entities.map(e => ({ id: e.id, name: e.name }));
|
||||
const prompt = `Analyse ce texte et résume brièvement l'évolution ou les actions de ces personnages/lieux: ${JSON.stringify(entityList)}\nTexte: "${truncate(text, 1000)}"`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
entityId: { type: Type.STRING },
|
||||
summary: { type: Type.STRING }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const updates = JSON.parse(response.text || "[]") as {entityId: string, summary: string}[];
|
||||
if (updates.length === 0) return entities;
|
||||
|
||||
return entities.map(entity => {
|
||||
const update = updates.find(u => u.entityId === entity.id);
|
||||
if (update) {
|
||||
const oldContext = entity.storyContext || "";
|
||||
return { ...entity, storyContext: truncate(oldContext + " | " + update.summary, 1000) };
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
} catch (e) { return entities; }
|
||||
};
|
||||
|
||||
export const transformText = async (
|
||||
text: string,
|
||||
mode: 'correct' | 'rewrite' | 'expand' | 'continue',
|
||||
context: string,
|
||||
user: UserProfile,
|
||||
onSuccess: () => void
|
||||
): Promise<string> => {
|
||||
if (!checkUsage(user)) return "Limite d'actions IA atteinte.";
|
||||
try {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
let prompt = `Action: ${mode}. Texte: ${text}. Contexte: ${truncate(context, 1000)}. Renvoie juste le texte transformé.`;
|
||||
const response = await ai.models.generateContent({ model: 'gemini-3-flash-preview', contents: prompt });
|
||||
onSuccess();
|
||||
return response.text?.trim() || text;
|
||||
} catch (e) { return text; }
|
||||
};
|
||||
|
||||
export const analyzeStyle = async (text: string) => "Style analysé";
|
||||
export const summarizeText = async (text: string) => "Résumé généré";
|
||||
Reference in New Issue
Block a user