mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-18 20:56:43 +02:00
Interface : passage de Next.js à une SPA React (Vite + React Router)
Le serveur étant réécrit en Rust, l'interface devient une application
React 19 côté navigateur, compilée par Vite dans dist/ et servie par le
serveur Rust (STATIC_DIR). Composants, styles et logique cliente
inchangés : seule la plomberie Next.js est remplacée.
- Outillage : Vite 8, @vitejs/plugin-react, Tailwind 4 via PostCSS,
React Router 7 (mode déclaratif), index.html, src/main.tsx, alias @/,
proxy /api → localhost:5643 en développement, ESLint sans plugin Next.
- Adaptations (src/shims) : next/link, next/navigation (useRouter avec
refresh, usePathname/useSearchParams suivant aussi les replaceState de
l'interface), next-auth/react (session lue sur /api/auth/session,
déconnexion par /api/auth/logout).
- Actions serveur → POST /api/actions/{nom} (src/lib/rpc.ts) : mêmes noms,
mêmes signatures, mêmes types. La connexion passe par /api/auth/login.
- Pages → table des routes (src/router.tsx) : données lues sur
/api/pages/{nom} avec Suspense (loading.tsx au premier chargement,
page conservée pendant une navigation ou un router.refresh()),
error.tsx en frontière d'erreur, not-found.tsx, titres d'onglet,
pages d'administration réservées aux administrateurs.
- Suppression du code serveur Next.js (routes /api, accès base, Qlik,
authentification, middleware…) ; types utiles à l'interface recopiés
dans src/types/ff.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG2LLJuswrrTTPKJmunEVf
This commit is contained in:
170 files changed
+2528
-24445
No files matched your search
@@ -1,17 +1,14 @@
|
||||
"use server";
|
||||
|
||||
/**
|
||||
* Actions des Paramètres — réservées aux administrateurs.
|
||||
* Actions des Paramètres — réservées aux administrateurs, exécutées par le
|
||||
* serveur Rust : `POST /api/actions/{nom}`.
|
||||
*
|
||||
* Aucun secret ne repart vers le navigateur : les mots de passe (base, Qlik) et
|
||||
* les clés d'IA restent dans `data/.db-config.json`. Un mot de passe laissé vide
|
||||
* dans le formulaire signifie « conserver celui qui est enregistré ».
|
||||
*/
|
||||
|
||||
import { Pool } from "pg";
|
||||
import { requireAdmin, verifierAdmin } from "@/lib/authz";
|
||||
import { CONFIG_FILE, readConfig, writeConfig } from "./config-file";
|
||||
import { diagnostiquerApiFf } from "./ff-api-diagnostic";
|
||||
import { callAction } from "@/lib/rpc";
|
||||
import type { FfSyncStatus } from "@/types/ff";
|
||||
|
||||
export interface ChampsBaseDeDonnees {
|
||||
host: string;
|
||||
@@ -37,181 +34,45 @@ export interface ParametresAffichables {
|
||||
ffApiBaseUrl: string;
|
||||
}
|
||||
|
||||
/** Décode un composant d'URL, ou le garde tel quel s'il n'est pas encodé proprement. */
|
||||
function decoder(valeur: string): string {
|
||||
try {
|
||||
return decodeURIComponent(valeur);
|
||||
} catch {
|
||||
return valeur;
|
||||
}
|
||||
export interface ResultatAction {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Découpe l'URL `postgres://` enregistrée en champs du formulaire. */
|
||||
function lireUrlPostgres(url: string) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return {
|
||||
host: u.hostname,
|
||||
port: u.port || "5432",
|
||||
database: decoder(u.pathname.slice(1)),
|
||||
user: decoder(u.username),
|
||||
password: decoder(u.password),
|
||||
ssl: url.includes("sslmode=require"),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("[Settings] URL de base enregistrée illisible :", (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function construireUrlPostgres(champs: ChampsBaseDeDonnees, motDePasse: string): string {
|
||||
const user = encodeURIComponent(champs.user.trim());
|
||||
const identifiants = motDePasse ? `${user}:${encodeURIComponent(motDePasse)}` : user;
|
||||
const port = champs.port.trim() || "5432";
|
||||
return `postgres://${identifiants}@${champs.host.trim()}:${port}/${champs.database.trim()}${champs.ssl ? "?sslmode=require" : ""}`;
|
||||
}
|
||||
|
||||
/** Mot de passe saisi, sinon celui de l'URL enregistrée. */
|
||||
async function motDePasseBase(champs: ChampsBaseDeDonnees): Promise<string> {
|
||||
if (champs.password) return champs.password;
|
||||
const { url } = await readConfig();
|
||||
return (url && lireUrlPostgres(url)?.password) || "";
|
||||
}
|
||||
/** Diagnostic de l'API FF Nancy (bouton « Tester »). */
|
||||
export type DiagnosticApiFf =
|
||||
| { success: true; url: string; ms: number; status: FfSyncStatus }
|
||||
| { success: false; url: string; error: string };
|
||||
|
||||
/** Réglages enregistrés, sans aucun secret. */
|
||||
export async function getParametresAffichables(): Promise<ParametresAffichables> {
|
||||
await requireAdmin();
|
||||
const config = await readConfig();
|
||||
const base = config.url ? lireUrlPostgres(config.url) : null;
|
||||
return {
|
||||
db: base
|
||||
? {
|
||||
host: base.host,
|
||||
port: base.port,
|
||||
database: base.database,
|
||||
user: base.user,
|
||||
ssl: base.ssl,
|
||||
motDePasseEnregistre: base.password !== "",
|
||||
}
|
||||
: null,
|
||||
qlik: {
|
||||
host: config.qlikHost ?? "",
|
||||
user: config.qlikUser ?? "",
|
||||
// Même ordre de priorité que getQlikConfig() : réglage enregistré, puis variable d'environnement.
|
||||
motDePasseEnregistre: Boolean(config.qlikPassword || process.env.QLIK_PWD),
|
||||
},
|
||||
ffApiBaseUrl: config.ffApiBaseUrl ?? "",
|
||||
};
|
||||
return callAction("getParametresAffichables", []);
|
||||
}
|
||||
|
||||
export async function testDatabaseConnection(champs: ChampsBaseDeDonnees) {
|
||||
const acces = await verifierAdmin();
|
||||
if (!acces.ok) return { success: false, error: acces.message };
|
||||
|
||||
const url = construireUrlPostgres(champs, await motDePasseBase(champs));
|
||||
console.log("Testing connection to:", url.replace(/:([^@]+)@/, ":****@"));
|
||||
const pool = new Pool({
|
||||
connectionString: url,
|
||||
connectionTimeoutMillis: 5000,
|
||||
});
|
||||
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
await client.query("SELECT 1");
|
||||
client.release();
|
||||
return { success: true };
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error("Database connection test failed:", errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
export async function testDatabaseConnection(champs: ChampsBaseDeDonnees): Promise<ResultatAction> {
|
||||
return callAction("testDatabaseConnection", [champs]);
|
||||
}
|
||||
|
||||
export async function saveDatabaseSettings(champs: ChampsBaseDeDonnees) {
|
||||
const acces = await verifierAdmin();
|
||||
if (!acces.ok) return { success: false, error: acces.message };
|
||||
|
||||
try {
|
||||
const url = construireUrlPostgres(champs, await motDePasseBase(champs));
|
||||
// Seule l'URL change : les clés d'IA et les réglages Qlik / API FF sont conservés.
|
||||
console.log(`[Settings] Saving config to ${CONFIG_FILE}`);
|
||||
await writeConfig({ url });
|
||||
console.log("[Settings] Database configuration saved successfully.");
|
||||
|
||||
// Refresh the shared DB instance immediately
|
||||
const { refreshDb } = await import("@/db");
|
||||
refreshDb();
|
||||
|
||||
return { success: true };
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error("Failed to save database configuration:", errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
export async function saveDatabaseSettings(champs: ChampsBaseDeDonnees): Promise<ResultatAction> {
|
||||
return callAction("saveDatabaseSettings", [champs]);
|
||||
}
|
||||
|
||||
/** Mot de passe vide ou absent : celui qui est enregistré est conservé. */
|
||||
export async function saveQlikSettings(qlikHost: string, qlikUser: string, qlikPassword?: string) {
|
||||
const acces = await verifierAdmin();
|
||||
if (!acces.ok) return { success: false, error: acces.message };
|
||||
|
||||
try {
|
||||
await writeConfig({ qlikHost, qlikUser, ...(qlikPassword ? { qlikPassword } : {}) });
|
||||
console.log("[Settings] Qlik configuration saved.");
|
||||
return { success: true };
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
console.error("[Settings] Failed to save Qlik config:", msg);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
export async function saveQlikSettings(qlikHost: string, qlikUser: string, qlikPassword?: string): Promise<ResultatAction> {
|
||||
return callAction("saveQlikSettings", [qlikHost, qlikUser, qlikPassword]);
|
||||
}
|
||||
|
||||
/** Mot de passe vide ou absent : on teste avec celui qui est enregistré. */
|
||||
export async function testQlikConnection(qlikHost: string, qlikUser: string, qlikPassword?: string) {
|
||||
const acces = await verifierAdmin();
|
||||
if (!acces.ok) return { success: false, error: acces.message };
|
||||
|
||||
try {
|
||||
const { getQlikConfig, qlikNtlmSession } = await import("@/lib/qlik-client");
|
||||
const enregistre = getQlikConfig();
|
||||
const cfg = { ...enregistre, user: qlikUser, password: qlikPassword || enregistre.password };
|
||||
if (qlikHost) cfg.host = qlikHost;
|
||||
await qlikNtlmSession(cfg);
|
||||
return { success: true };
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
export async function testQlikConnection(qlikHost: string, qlikUser: string, qlikPassword?: string): Promise<ResultatAction> {
|
||||
return callAction("testQlikConnection", [qlikHost, qlikUser, qlikPassword]);
|
||||
}
|
||||
|
||||
/** Enregistre l'URL de l'API FF Nancy. Chaîne vide = revenir au défaut. */
|
||||
export async function saveFfApiSettings(ffApiBaseUrl: string) {
|
||||
const acces = await verifierAdmin();
|
||||
if (!acces.ok) return { success: false, error: acces.message };
|
||||
|
||||
try {
|
||||
const cleaned = ffApiBaseUrl.trim().replace(/\/+$/, "");
|
||||
if (cleaned && !/^https?:\/\//i.test(cleaned)) {
|
||||
return { success: false, error: "L'URL doit commencer par http:// ou https://" };
|
||||
}
|
||||
await writeConfig({ ffApiBaseUrl: cleaned || undefined });
|
||||
// Sans cela, l'ancienne URL resterait servie jusqu'à 30 s après la sauvegarde.
|
||||
const { resetFfApiBaseCache } = await import("@/lib/api-ff-client");
|
||||
resetFfApiBaseCache();
|
||||
console.log(`[Settings] FF API base URL saved: ${cleaned || "(défaut)"}`);
|
||||
return { success: true };
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
console.error("[Settings] Failed to save FF API config:", msg);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
export async function saveFfApiSettings(ffApiBaseUrl: string): Promise<ResultatAction> {
|
||||
return callAction("saveFfApiSettings", [ffApiBaseUrl]);
|
||||
}
|
||||
|
||||
/** Teste l'API FF Nancy (voir `diagnostiquerApiFf`). */
|
||||
export async function testFfApiConnection(ffApiBaseUrl?: string): Promise<Awaited<ReturnType<typeof diagnostiquerApiFf>>> {
|
||||
const acces = await verifierAdmin();
|
||||
if (!acces.ok) return { success: false, url: "", error: acces.message };
|
||||
return diagnostiquerApiFf(ffApiBaseUrl);
|
||||
/** Teste l'API FF Nancy (diagnostic détaillé : DNS, refus, délai, code HTTP). */
|
||||
export async function testFfApiConnection(ffApiBaseUrl?: string): Promise<DiagnosticApiFf> {
|
||||
return callAction("testFfApiConnection", [ffApiBaseUrl]);
|
||||
}
|
||||
Reference in new issue
Block a user