mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 12:41:27 +00:00
438 lines
14 KiB
JavaScript
438 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* CLI pour créer un ou plusieurs établissements N3WT.
|
|
*
|
|
* Usage interactif :
|
|
* node scripts/create-establishment.js
|
|
*
|
|
* Usage batch (fichier JSON) :
|
|
* node scripts/create-establishment.js --file batch.json
|
|
*
|
|
* Format du fichier batch :
|
|
* {
|
|
* "backendUrl": "http://localhost:8080", // optionnel (fallback : URL_DJANGO dans conf/backend.env)
|
|
* "apiKey": "TOk3n1234!!", // optionnel (fallback : WEBHOOK_API_KEY dans conf/backend.env)
|
|
* "establishments": [
|
|
* {
|
|
* "name": "École Dupont",
|
|
* "address": "1 rue de la Paix, Paris",
|
|
* "total_capacity": 300,
|
|
* "establishment_type": [1, 2], // 1=Maternelle, 2=Primaire, 3=Secondaire
|
|
* "evaluation_frequency": 1, // 1=Trimestre, 2=Semestre, 3=Année
|
|
* "licence_code": "LIC001", // optionnel
|
|
* "directeur": {
|
|
* "email": "directeur@dupont.fr",
|
|
* "password": "motdepasse123",
|
|
* "last_name": "Dupont",
|
|
* "first_name": "Jean"
|
|
* }
|
|
* }
|
|
* ]
|
|
* }
|
|
*/
|
|
|
|
const readline = require("readline");
|
|
const http = require("http");
|
|
const https = require("https");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
// ── Lecture de conf/backend.env ───────────────────────────────────────────────
|
|
|
|
function loadBackendEnv() {
|
|
const envPath = path.join(__dirname, "..", "conf", "backend.env");
|
|
if (!fs.existsSync(envPath)) return;
|
|
|
|
const lines = fs.readFileSync(envPath, "utf8").split("\n");
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
const eqIdx = trimmed.indexOf("=");
|
|
if (eqIdx === -1) continue;
|
|
const key = trimmed.slice(0, eqIdx).trim();
|
|
let value = trimmed.slice(eqIdx + 1).trim();
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
(value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
// Ne pas écraser les variables déjà définies dans l'environnement
|
|
if (!(key in process.env)) {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Helpers readline ──────────────────────────────────────────────────────────
|
|
|
|
let rl;
|
|
|
|
function getRL() {
|
|
if (!rl) {
|
|
rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
}
|
|
return rl;
|
|
}
|
|
|
|
function ask(question, defaultValue) {
|
|
const suffix =
|
|
defaultValue != null && defaultValue !== "" ? ` (${defaultValue})` : "";
|
|
return new Promise((resolve) => {
|
|
getRL().question(`${question}${suffix}: `, (answer) => {
|
|
resolve(
|
|
answer.trim() || (defaultValue != null ? String(defaultValue) : ""),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
function askRequired(question) {
|
|
return new Promise((resolve) => {
|
|
const prompt = () => {
|
|
getRL().question(`${question}: `, (answer) => {
|
|
if (!answer.trim()) {
|
|
console.log(" ⚠ Ce champ est obligatoire.");
|
|
prompt();
|
|
} else {
|
|
resolve(answer.trim());
|
|
}
|
|
});
|
|
};
|
|
prompt();
|
|
});
|
|
}
|
|
|
|
function askChoices(question, choices) {
|
|
return new Promise((resolve) => {
|
|
console.log(`\n${question}`);
|
|
choices.forEach((c, i) => console.log(` ${i + 1}. ${c.label}`));
|
|
const prompt = () => {
|
|
getRL().question(
|
|
"Choix (numéros séparés par des virgules): ",
|
|
(answer) => {
|
|
const nums = answer
|
|
.split(",")
|
|
.map((s) => parseInt(s.trim(), 10))
|
|
.filter((n) => n >= 1 && n <= choices.length);
|
|
if (nums.length === 0) {
|
|
console.log(" ⚠ Sélectionnez au moins une option.");
|
|
prompt();
|
|
} else {
|
|
resolve(nums.map((n) => choices[n - 1].value));
|
|
}
|
|
},
|
|
);
|
|
};
|
|
prompt();
|
|
});
|
|
}
|
|
|
|
function askChoice(question, choices, defaultIndex) {
|
|
return new Promise((resolve) => {
|
|
console.log(`\n${question}`);
|
|
choices.forEach((c, i) =>
|
|
console.log(
|
|
` ${i + 1}. ${c.label}${i === defaultIndex ? " (défaut)" : ""}`,
|
|
),
|
|
);
|
|
const prompt = () => {
|
|
getRL().question(`Choix (1-${choices.length}): `, (answer) => {
|
|
if (!answer.trim() && defaultIndex != null) {
|
|
resolve(choices[defaultIndex].value);
|
|
return;
|
|
}
|
|
const n = parseInt(answer.trim(), 10);
|
|
if (n >= 1 && n <= choices.length) {
|
|
resolve(choices[n - 1].value);
|
|
} else {
|
|
console.log(" ⚠ Choix invalide.");
|
|
prompt();
|
|
}
|
|
});
|
|
};
|
|
prompt();
|
|
});
|
|
}
|
|
|
|
// ── HTTP ──────────────────────────────────────────────────────────────────────
|
|
|
|
function postJSON(url, data, apiKey) {
|
|
return new Promise((resolve, reject) => {
|
|
const parsed = new URL(url);
|
|
const mod = parsed.protocol === "https:" ? https : http;
|
|
const body = JSON.stringify(data);
|
|
|
|
const req = mod.request(
|
|
{
|
|
hostname: parsed.hostname,
|
|
port: parsed.port,
|
|
path: parsed.pathname,
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": Buffer.byteLength(body),
|
|
"X-API-Key": apiKey,
|
|
},
|
|
},
|
|
(res) => {
|
|
let raw = "";
|
|
res.on("data", (chunk) => (raw += chunk));
|
|
res.on("end", () => {
|
|
try {
|
|
resolve({ status: res.statusCode, data: JSON.parse(raw) });
|
|
} catch {
|
|
resolve({ status: res.statusCode, data: raw });
|
|
}
|
|
});
|
|
},
|
|
);
|
|
|
|
req.on("error", reject);
|
|
req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// ── Création d'un établissement ───────────────────────────────────────────────
|
|
|
|
async function createEstablishment(backendUrl, apiKey, payload) {
|
|
const url = `${backendUrl.replace(/\/$/, "")}/Establishment/establishments`;
|
|
const res = await postJSON(url, payload, apiKey);
|
|
return res;
|
|
}
|
|
|
|
// ── Validation basique d'un enregistrement batch ──────────────────────────────
|
|
|
|
function validateRecord(record, index) {
|
|
const errors = [];
|
|
const label = record.name ? `"${record.name}"` : `#${index + 1}`;
|
|
|
|
if (!record.name) errors.push("name manquant");
|
|
if (!record.address) errors.push("address manquant");
|
|
if (!record.total_capacity) errors.push("total_capacity manquant");
|
|
if (
|
|
!Array.isArray(record.establishment_type) ||
|
|
record.establishment_type.length === 0
|
|
)
|
|
errors.push("establishment_type manquant ou vide");
|
|
if (!record.directeur) errors.push("directeur manquant");
|
|
else {
|
|
if (!record.directeur.email) errors.push("directeur.email manquant");
|
|
if (!record.directeur.password) errors.push("directeur.password manquant");
|
|
if (!record.directeur.last_name)
|
|
errors.push("directeur.last_name manquant");
|
|
if (!record.directeur.first_name)
|
|
errors.push("directeur.first_name manquant");
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
throw new Error(`Établissement ${label} invalide : ${errors.join(", ")}`);
|
|
}
|
|
}
|
|
|
|
// ── Mode batch ────────────────────────────────────────────────────────────────
|
|
|
|
async function runBatch(filePath) {
|
|
const absPath = path.resolve(filePath);
|
|
if (!fs.existsSync(absPath)) {
|
|
console.error(`❌ Fichier introuvable : ${absPath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
let batch;
|
|
try {
|
|
batch = JSON.parse(fs.readFileSync(absPath, "utf8"));
|
|
} catch (err) {
|
|
console.error(`❌ Fichier JSON invalide : ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const backendUrl =
|
|
batch.backendUrl || process.env.URL_DJANGO || "http://localhost:8080";
|
|
const apiKey = batch.apiKey || process.env.WEBHOOK_API_KEY;
|
|
|
|
if (!apiKey) {
|
|
console.error(
|
|
"❌ apiKey manquant dans le fichier batch ou la variable WEBHOOK_API_KEY.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const establishments = batch.establishments;
|
|
if (!Array.isArray(establishments) || establishments.length === 0) {
|
|
console.error("❌ Le fichier batch ne contient aucun établissement.");
|
|
process.exit(1);
|
|
}
|
|
|
|
// Validation préalable de tous les enregistrements
|
|
establishments.forEach((record, i) => validateRecord(record, i));
|
|
|
|
console.log(
|
|
`\n📋 Batch : ${establishments.length} établissement(s) à créer sur ${backendUrl}\n`,
|
|
);
|
|
|
|
let success = 0;
|
|
let failure = 0;
|
|
|
|
for (let i = 0; i < establishments.length; i++) {
|
|
const record = establishments[i];
|
|
const label = `[${i + 1}/${establishments.length}] ${record.name}`;
|
|
process.stdout.write(` ${label} ... `);
|
|
|
|
try {
|
|
const res = await createEstablishment(backendUrl, apiKey, record);
|
|
if (res.status === 201) {
|
|
console.log(`✅ (ID: ${res.data.id})`);
|
|
success++;
|
|
} else {
|
|
console.log(`❌ HTTP ${res.status}`);
|
|
console.error(` ${JSON.stringify(res.data)}`);
|
|
failure++;
|
|
}
|
|
} catch (err) {
|
|
console.log(`❌ Erreur réseau : ${err.message}`);
|
|
failure++;
|
|
}
|
|
}
|
|
|
|
console.log(`\nRésultat : ${success} créé(s), ${failure} échec(s).`);
|
|
if (failure > 0) process.exit(1);
|
|
}
|
|
|
|
// ── Mode interactif ───────────────────────────────────────────────────────────
|
|
|
|
async function runInteractive() {
|
|
console.log("╔══════════════════════════════════════════╗");
|
|
console.log("║ Création d'un établissement N3WT ║");
|
|
console.log("╚══════════════════════════════════════════╝\n");
|
|
|
|
const backendUrl = await ask(
|
|
"URL du backend Django",
|
|
process.env.URL_DJANGO || "http://localhost:8080",
|
|
);
|
|
const apiKey = await ask(
|
|
"Clé API webhook (WEBHOOK_API_KEY)",
|
|
process.env.WEBHOOK_API_KEY || "",
|
|
);
|
|
if (!apiKey) {
|
|
console.error("❌ La clé API est obligatoire.");
|
|
getRL().close();
|
|
process.exit(1);
|
|
}
|
|
|
|
// --- Établissement ---
|
|
console.log("\n── Établissement ──");
|
|
const name = await askRequired("Nom de l'établissement");
|
|
const address = await askRequired("Adresse");
|
|
const totalCapacity = parseInt(await askRequired("Capacité totale"), 10);
|
|
|
|
const establishmentType = await askChoices("Type(s) de structure:", [
|
|
{ label: "Maternelle", value: 1 },
|
|
{ label: "Primaire", value: 2 },
|
|
{ label: "Secondaire", value: 3 },
|
|
]);
|
|
|
|
const evaluationFrequency = await askChoice(
|
|
"Fréquence d'évaluation:",
|
|
[
|
|
{ label: "Trimestre", value: 1 },
|
|
{ label: "Semestre", value: 2 },
|
|
{ label: "Année", value: 3 },
|
|
],
|
|
0,
|
|
);
|
|
|
|
const licenceCode = await ask("Code licence (optionnel)", "");
|
|
|
|
// --- Directeur (admin) ---
|
|
console.log("\n── Compte directeur (admin) ──");
|
|
const directorEmail = await askRequired("Email du directeur");
|
|
const directorPassword = await askRequired("Mot de passe");
|
|
const directorLastName = await askRequired("Nom de famille");
|
|
const directorFirstName = await askRequired("Prénom");
|
|
|
|
// --- Récapitulatif ---
|
|
console.log("\n── Récapitulatif ──");
|
|
console.log(` Établissement : ${name}`);
|
|
console.log(` Adresse : ${address}`);
|
|
console.log(` Capacité : ${totalCapacity}`);
|
|
console.log(` Type(s) : ${establishmentType.join(", ")}`);
|
|
console.log(` Évaluation : ${evaluationFrequency}`);
|
|
console.log(
|
|
` Directeur : ${directorFirstName} ${directorLastName} <${directorEmail}>`,
|
|
);
|
|
|
|
const confirm = await ask("\nConfirmer la création ? (o/n)", "o");
|
|
if (confirm.toLowerCase() !== "o") {
|
|
console.log("Annulé.");
|
|
getRL().close();
|
|
process.exit(0);
|
|
}
|
|
|
|
const payload = {
|
|
name,
|
|
address,
|
|
total_capacity: totalCapacity,
|
|
establishment_type: establishmentType,
|
|
evaluation_frequency: evaluationFrequency,
|
|
...(licenceCode && { licence_code: licenceCode }),
|
|
directeur: {
|
|
email: directorEmail,
|
|
password: directorPassword,
|
|
last_name: directorLastName,
|
|
first_name: directorFirstName,
|
|
},
|
|
};
|
|
|
|
console.log("\nCréation en cours...");
|
|
try {
|
|
const res = await createEstablishment(backendUrl, apiKey, payload);
|
|
if (res.status === 201) {
|
|
console.log("\n✅ Établissement créé avec succès !");
|
|
console.log(` ID : ${res.data.id}`);
|
|
console.log(` Nom : ${res.data.name}`);
|
|
} else {
|
|
console.error(`\n❌ Erreur (HTTP ${res.status}):`);
|
|
console.error(JSON.stringify(res.data, null, 2));
|
|
process.exit(1);
|
|
}
|
|
} catch (err) {
|
|
console.error("\n❌ Erreur réseau:", err.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
getRL().close();
|
|
}
|
|
|
|
// ── Point d'entrée ────────────────────────────────────────────────────────────
|
|
|
|
async function main() {
|
|
loadBackendEnv();
|
|
|
|
const args = process.argv.slice(2);
|
|
const fileIndex = args.indexOf("--file");
|
|
|
|
if (fileIndex !== -1) {
|
|
const filePath = args[fileIndex + 1];
|
|
if (!filePath) {
|
|
console.error(
|
|
"❌ Argument --file manquant. Usage : --file <chemin/vers/batch.json>",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
await runBatch(filePath);
|
|
} else {
|
|
await runInteractive();
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("❌ Erreur inattendue:", err.message);
|
|
process.exit(1);
|
|
});
|