Initial local backup snapshot

This commit is contained in:
Selecta Keke 2026-06-24 00:02:55 -03:00
commit acd9e14ba5
367 changed files with 118038 additions and 0 deletions

952
server/operationalTour.ts Normal file
View file

@ -0,0 +1,952 @@
import { z } from "zod";
import type { Request } from "express";
import { COOKIE_NAME } from "@shared/const";
import type { User } from "../drizzle/schema";
import { createSessionToken, loginLocalUser } from "./_core/auth";
import * as db from "./db";
export const OPERATIONAL_TOUR_SETTINGS_KEY = "system.operationalTour.settings";
export const OPERATIONAL_TOUR_HISTORY_KEY = "system.operationalTour.history";
export const operationalTourEnvironmentSchema = z.enum(["production", "secondary"]);
export type OperationalTourEnvironment = z.infer<typeof operationalTourEnvironmentSchema>;
export const operationalTourAuthModeSchema = z.enum(["local", "oauth"]);
export type OperationalTourAuthMode = z.infer<typeof operationalTourAuthModeSchema>;
export const operationalTourStepKindSchema = z.enum(["technical", "user"]);
export type OperationalTourStepKind = z.infer<typeof operationalTourStepKindSchema>;
const operationalTourTargetSchema = z.object({
baseUrl: z.string().trim().optional().default(""),
associationEmail: z.string().trim().optional().default(""),
associationPassword: z.string().trim().optional().default(""),
associationAuthMode: operationalTourAuthModeSchema.optional().default("local"),
accueilEmail: z.string().trim().optional().default(""),
accueilPassword: z.string().trim().optional().default(""),
accueilAuthMode: operationalTourAuthModeSchema.optional().default("local"),
adminEmail: z.string().trim().optional().default(""),
adminPassword: z.string().trim().optional().default(""),
adminAuthMode: operationalTourAuthModeSchema.optional().default("local"),
});
const EMPTY_TARGET = {
baseUrl: "",
associationEmail: "",
associationPassword: "",
associationAuthMode: "local",
accueilEmail: "",
accueilPassword: "",
accueilAuthMode: "local",
adminEmail: "",
adminPassword: "",
adminAuthMode: "local",
} as const;
const operationalTourSettingsSchema = z.object({
defaultEnvironment: operationalTourEnvironmentSchema.default("production"),
targets: z.object({
production: operationalTourTargetSchema.default(EMPTY_TARGET),
secondary: operationalTourTargetSchema.default(EMPTY_TARGET),
}),
});
export type OperationalTourSettings = z.infer<typeof operationalTourSettingsSchema>;
const operationalTourStepSchema = z.object({
id: z.string(),
label: z.string(),
kind: operationalTourStepKindSchema.default("technical"),
status: z.enum(["ok", "warning", "error"]),
durationMs: z.number().int().nonnegative(),
message: z.string(),
details: z.string().optional(),
url: z.string().optional(),
critical: z.boolean().default(true),
});
export type OperationalTourStep = z.infer<typeof operationalTourStepSchema>;
const operationalTourReportSchema = z.object({
id: z.string(),
environment: operationalTourEnvironmentSchema,
environmentLabel: z.string(),
baseUrl: z.string(),
status: z.enum(["ok", "warning", "error"]),
startedAt: z.string(),
finishedAt: z.string(),
durationMs: z.number().int().nonnegative(),
initiatedBy: z.object({
userId: z.number().int().positive(),
name: z.string(),
email: z.string().optional(),
}),
summary: z.object({
total: z.number().int().nonnegative(),
ok: z.number().int().nonnegative(),
warning: z.number().int().nonnegative(),
error: z.number().int().nonnegative(),
}),
steps: z.array(operationalTourStepSchema),
});
export type OperationalTourReport = z.infer<typeof operationalTourReportSchema>;
const OPERATIONAL_TOUR_HISTORY_MAX_BYTES = 60_000;
const OPERATIONAL_TOUR_HISTORY_MAX_REPORTS = 12;
const OPERATIONAL_TOUR_DUPLICATE_WINDOW_MS = 15_000;
const EMPTY_SETTINGS: OperationalTourSettings = {
defaultEnvironment: "production",
targets: {
production: {
baseUrl: "https://www.portail-association973.com",
associationEmail: "",
associationPassword: "",
associationAuthMode: "local",
accueilEmail: "",
accueilPassword: "",
accueilAuthMode: "local",
adminEmail: "",
adminPassword: "",
adminAuthMode: "local",
},
secondary: {
baseUrl: "",
associationEmail: "",
associationPassword: "",
associationAuthMode: "local",
accueilEmail: "",
accueilPassword: "",
accueilAuthMode: "local",
adminEmail: "",
adminPassword: "",
adminAuthMode: "local",
},
},
};
type PageCheckOptions = {
id: string;
label: string;
kind: OperationalTourStepKind;
url: string;
cookieHeader?: string | null;
markers?: string[];
expectedPath?: string;
critical?: boolean;
};
type PageCheckResult = OperationalTourStep;
function normalizeBaseUrl(value: string | undefined) {
return String(value || "").trim().replace(/\/+$/, "");
}
function normalizeEmail(value: string | undefined) {
return String(value || "").trim().toLowerCase();
}
function buildRequestOrigin(req: Request) {
const forwardedProto = typeof req.headers["x-forwarded-proto"] === "string" ? req.headers["x-forwarded-proto"] : undefined;
const forwardedHost = typeof req.headers["x-forwarded-host"] === "string" ? req.headers["x-forwarded-host"] : undefined;
const protocol = forwardedProto || req.protocol || "http";
const host = forwardedHost || req.get?.("host") || "";
return host ? `${protocol}://${host}` : "";
}
function isLoginPage(body: string, finalUrl: string) {
return finalUrl.includes("/login") || body.includes("Connexion requise") || body.includes("Se connecter");
}
function normalizePathname(url: string) {
try {
const pathname = new URL(url).pathname || "/";
return pathname === "/" ? "/" : pathname.replace(/\/+$/, "");
} catch {
return url;
}
}
function hasReactShell(body: string, contentType: string | null) {
const lowerBody = body.toLowerCase();
const lowerType = String(contentType || "").toLowerCase();
return (
lowerType.includes("text/html") &&
(lowerBody.includes("<!doctype html") || lowerBody.includes("<html")) &&
(lowerBody.includes("<title>") || lowerBody.includes("script type=\"module\"") || lowerBody.includes("id=\"manus-runtime\""))
);
}
function buildStaticStep(input: Omit<OperationalTourStep, "durationMs"> & { durationMs?: number }): OperationalTourStep {
return {
...input,
durationMs: input.durationMs ?? 0,
};
}
async function fetchPageCheck(options: PageCheckOptions): Promise<PageCheckResult> {
const started = Date.now();
try {
const response = await fetch(options.url, {
headers: options.cookieHeader ? { cookie: options.cookieHeader } : undefined,
redirect: "follow",
});
const body = await response.text();
const finalUrl = response.url || options.url;
const contentType = response.headers.get("content-type");
const expectedPath = normalizePathname(options.expectedPath || options.url);
const finalPath = normalizePathname(finalUrl);
const markers = options.markers || [];
const matchedMarker = markers.find((marker) => body.includes(marker));
const routeMatches = expectedPath === "/" ? finalPath === "/" : finalPath === expectedPath || finalPath.startsWith(`${expectedPath}/`);
const reactShell = hasReactShell(body, contentType);
if (!response.ok) {
return {
id: options.id,
label: options.label,
kind: options.kind,
status: "error",
durationMs: Date.now() - started,
message: `HTTP ${response.status}`,
details: finalUrl,
url: options.url,
critical: options.critical ?? true,
};
}
if (options.cookieHeader && isLoginPage(body, finalUrl)) {
return {
id: options.id,
label: options.label,
kind: options.kind,
status: "error",
durationMs: Date.now() - started,
message: "Authentification non reconnue",
details: finalUrl,
url: options.url,
critical: options.critical ?? true,
};
}
if (!routeMatches) {
return {
id: options.id,
label: options.label,
kind: options.kind,
status: "warning",
durationMs: Date.now() - started,
message: "Page servie sur un chemin inattendu",
details: `${finalUrl} (attendu: ${expectedPath})`,
url: options.url,
critical: options.critical ?? true,
};
}
if (matchedMarker) {
return {
id: options.id,
label: options.label,
kind: options.kind,
status: "ok",
durationMs: Date.now() - started,
message: `Contrôle réussi (${matchedMarker})`,
details: finalUrl,
url: options.url,
critical: options.critical ?? true,
};
}
if (reactShell) {
return {
id: options.id,
label: options.label,
kind: options.kind,
status: "ok",
durationMs: Date.now() - started,
message: "Page accessible, rendu client servi",
details: finalUrl,
url: options.url,
critical: options.critical ?? true,
};
}
return {
id: options.id,
label: options.label,
kind: options.kind,
status: markers.length > 0 ? "warning" : "ok",
durationMs: Date.now() - started,
message: markers.length > 0 ? "Page accessible, marqueur visuel non confirmé" : "Contrôle réussi",
details: finalUrl,
url: options.url,
critical: options.critical ?? true,
};
} catch (error) {
return {
id: options.id,
label: options.label,
kind: options.kind,
status: "error",
durationMs: Date.now() - started,
message: error instanceof Error ? error.message : "Erreur réseau",
url: options.url,
critical: options.critical ?? true,
};
}
}
async function fetchJsonCheck(options: {
id: string;
label: string;
kind: OperationalTourStepKind;
url: string;
cookieHeader?: string | null;
markers?: string[];
critical?: boolean;
}) {
const started = Date.now();
try {
const response = await fetch(options.url, {
headers: {
accept: "application/json",
...(options.cookieHeader ? { cookie: options.cookieHeader } : {}),
},
redirect: "follow",
});
const body = await response.text();
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
const matchesMarker = (options.markers || []).some((marker) => body.includes(marker));
if (!response.ok) {
return buildStaticStep({
id: options.id,
label: options.label,
kind: options.kind,
status: "error",
message: `HTTP ${response.status}`,
details: response.url || options.url,
url: options.url,
critical: options.critical ?? true,
durationMs: Date.now() - started,
});
}
if (!contentType.includes("application/json")) {
return buildStaticStep({
id: options.id,
label: options.label,
kind: options.kind,
status: "warning",
message: "Réponse non JSON",
details: response.url || options.url,
url: options.url,
critical: options.critical ?? true,
durationMs: Date.now() - started,
});
}
return buildStaticStep({
id: options.id,
label: options.label,
kind: options.kind,
status: matchesMarker || !(options.markers || []).length ? "ok" : "warning",
message: matchesMarker || !(options.markers || []).length ? "API accessible et réponse JSON valide" : "API accessible, signature JSON à confirmer",
details: response.url || options.url,
url: options.url,
critical: options.critical ?? true,
durationMs: Date.now() - started,
});
} catch (error) {
return buildStaticStep({
id: options.id,
label: options.label,
kind: options.kind,
status: "error",
message: error instanceof Error ? error.message : "Erreur réseau",
url: options.url,
critical: options.critical ?? true,
durationMs: Date.now() - started,
});
}
}
async function fetchHomeDiagnostics(baseUrl: string) {
const started = Date.now();
try {
const response = await fetch(`${baseUrl}/`, { redirect: "follow" });
const body = await response.text();
return {
durationMs: Date.now() - started,
body,
headers: response.headers,
ok: response.ok,
finalUrl: response.url || `${baseUrl}/`,
};
} catch (error) {
return {
durationMs: Date.now() - started,
body: "",
headers: new Headers(),
ok: false,
finalUrl: `${baseUrl}/`,
error: error instanceof Error ? error.message : "Erreur réseau",
};
}
}
async function createConfiguredSessionCookie(email: string, password?: string) {
const normalizedEmail = normalizeEmail(email);
const normalizedPassword = String(password || "");
let user;
if (normalizedPassword) {
user = await loginLocalUser({ email: normalizedEmail, password: normalizedPassword });
} else {
const existingUser = await db.getUserByEmail(normalizedEmail);
if (!existingUser) {
throw new Error("Compte introuvable");
}
if (!existingUser.isActive) {
throw new Error("Compte inactif");
}
if (!existingUser.loginMethod || !existingUser.loginMethod.endsWith("_oauth")) {
throw new Error("Mot de passe requis pour ce compte");
}
user = existingUser;
}
const token = await createSessionToken(user);
return `${COOKIE_NAME}=${token}`;
}
async function tryFindPublicProfileUrl(baseUrl: string) {
try {
const response = await fetch(`${baseUrl}/associations`, { redirect: "follow" });
if (!response.ok) return null;
const body = await response.text();
const match = body.match(/href="(\/associations\/[^"]+)"/);
if (match) return `${baseUrl}${match[1]}`;
} catch {
// fallback below
}
try {
const directory = await db.listAssociationDirectoryMapEntries({ limit: 1 });
const firstEntry = directory.data?.[0];
return firstEntry?.id ? `${baseUrl}/associations/${firstEntry.id}` : null;
} catch {
return null;
}
}
function computeReportStatus(steps: OperationalTourStep[]): OperationalTourReport["status"] {
const hasCriticalError = steps.some((step) => step.critical !== false && step.status === "error");
if (hasCriticalError) return "error";
const hasWarning = steps.some((step) => step.status !== "ok");
return hasWarning ? "warning" : "ok";
}
function summarizeSteps(steps: OperationalTourStep[]) {
return {
total: steps.length,
ok: steps.filter((step) => step.status === "ok").length,
warning: steps.filter((step) => step.status === "warning").length,
error: steps.filter((step) => step.status === "error").length,
};
}
function summarizeStepsByKind(steps: OperationalTourStep[], kind: OperationalTourStepKind) {
return summarizeSteps(steps.filter((step) => step.kind === kind));
}
export function sanitizeOperationalTourSettings(input?: unknown): OperationalTourSettings {
const parsed = operationalTourSettingsSchema.safeParse(input);
if (!parsed.success) return EMPTY_SETTINGS;
return {
defaultEnvironment: parsed.data.defaultEnvironment,
targets: {
production: {
...EMPTY_SETTINGS.targets.production,
...parsed.data.targets.production,
baseUrl: normalizeBaseUrl(parsed.data.targets.production.baseUrl) || EMPTY_SETTINGS.targets.production.baseUrl,
associationAuthMode: parsed.data.targets.production.associationAuthMode || "local",
accueilAuthMode: parsed.data.targets.production.accueilAuthMode || "local",
adminAuthMode: parsed.data.targets.production.adminAuthMode || "local",
},
secondary: {
...EMPTY_SETTINGS.targets.secondary,
...parsed.data.targets.secondary,
baseUrl: normalizeBaseUrl(parsed.data.targets.secondary.baseUrl),
associationAuthMode: parsed.data.targets.secondary.associationAuthMode || "local",
accueilAuthMode: parsed.data.targets.secondary.accueilAuthMode || "local",
adminAuthMode: parsed.data.targets.secondary.adminAuthMode || "local",
},
},
};
}
export async function getOperationalTourSettings() {
const raw = await db.getPortalSetting(OPERATIONAL_TOUR_SETTINGS_KEY);
if (!raw) return EMPTY_SETTINGS;
try {
return sanitizeOperationalTourSettings(JSON.parse(raw));
} catch {
return EMPTY_SETTINGS;
}
}
export async function saveOperationalTourSettings(settings: OperationalTourSettings) {
const sanitized = sanitizeOperationalTourSettings(settings);
await db.setPortalSetting(
OPERATIONAL_TOUR_SETTINGS_KEY,
JSON.stringify(sanitized),
"Configuration du tour opérationnel du site"
);
return sanitized;
}
export async function getOperationalTourHistory() {
const raw = await db.getPortalSetting(OPERATIONAL_TOUR_HISTORY_KEY);
if (!raw) return [] as OperationalTourReport[];
try {
const parsed = z.array(operationalTourReportSchema).parse(JSON.parse(raw));
return parsed;
} catch {
return [] as OperationalTourReport[];
}
}
function compactOperationalTourHistory(history: OperationalTourReport[]) {
const compactStep = (step: OperationalTourStep): OperationalTourStep => ({
id: step.id,
label: step.label,
kind: step.kind,
status: step.status,
durationMs: step.durationMs,
message: step.message,
critical: step.critical ?? true,
...(step.status !== "ok" && step.details ? { details: step.details } : {}),
...(step.status !== "ok" && step.url ? { url: step.url } : {}),
});
const buildSignature = (report: OperationalTourReport) => JSON.stringify({
environment: report.environment,
baseUrl: report.baseUrl,
status: report.status,
summary: report.summary,
steps: report.steps.map((step) => ({
id: step.id,
kind: step.kind,
status: step.status,
message: step.message,
details: step.details || "",
})),
});
const dedupedHistory: OperationalTourReport[] = [];
for (const report of history) {
const previous = dedupedHistory[dedupedHistory.length - 1];
if (previous && previous.environment === report.environment) {
const previousTime = new Date(previous.finishedAt).getTime();
const currentTime = new Date(report.finishedAt).getTime();
if (
Number.isFinite(previousTime) &&
Number.isFinite(currentTime) &&
Math.abs(currentTime - previousTime) <= OPERATIONAL_TOUR_DUPLICATE_WINDOW_MS &&
buildSignature(previous) === buildSignature(report)
) {
dedupedHistory[dedupedHistory.length - 1] = report;
continue;
}
}
dedupedHistory.push(report);
}
let reports = dedupedHistory.slice(0, OPERATIONAL_TOUR_HISTORY_MAX_REPORTS).map((report) => ({
...report,
steps: report.steps.map(compactStep),
}));
while (reports.length > 0) {
const serialized = JSON.stringify(reports);
if (Buffer.byteLength(serialized, "utf8") <= OPERATIONAL_TOUR_HISTORY_MAX_BYTES) {
return reports;
}
reports = reports.slice(0, -1);
}
return [];
}
export async function saveOperationalTourHistory(history: OperationalTourReport[]) {
await db.setPortalSetting(
OPERATIONAL_TOUR_HISTORY_KEY,
JSON.stringify(compactOperationalTourHistory(history)),
"Historique du tour opérationnel du site"
);
}
export async function clearOperationalTourHistory() {
const history = await getOperationalTourHistory();
const seen = new Set<OperationalTourEnvironment>();
const retained = history.filter((report) => {
if (seen.has(report.environment)) {
return false;
}
seen.add(report.environment);
return true;
});
await saveOperationalTourHistory(retained);
return retained;
}
export async function runOperationalTour(params: {
environment: OperationalTourEnvironment;
req: Request;
currentUser: User;
}) {
const startedAt = new Date();
const settings = await getOperationalTourSettings();
const target = settings.targets[params.environment];
const baseUrl = normalizeBaseUrl(target.baseUrl);
const environmentLabel = params.environment === "production" ? "Production" : "Préproduction / Local";
const steps: OperationalTourStep[] = [];
if (!baseUrl) {
const report: OperationalTourReport = {
id: `tour-${Date.now()}`,
environment: params.environment,
environmentLabel,
baseUrl: "",
status: "error",
startedAt: startedAt.toISOString(),
finishedAt: new Date().toISOString(),
durationMs: 0,
initiatedBy: {
userId: params.currentUser.id,
name: params.currentUser.name || params.currentUser.email || `Utilisateur #${params.currentUser.id}`,
email: params.currentUser.email || undefined,
},
summary: { total: 1, ok: 0, warning: 0, error: 1 },
steps: [
{
id: "configuration",
label: "Configuration de lenvironnement",
kind: "technical",
status: "error",
durationMs: 0,
message: "URL cible manquante",
details: `Aucune URL nest configurée pour ${environmentLabel.toLowerCase()}.`,
critical: true,
},
],
};
const nextHistory = [report, ...(await getOperationalTourHistory())];
await saveOperationalTourHistory(nextHistory);
return report;
}
const currentOrigin = buildRequestOrigin(params.req);
const isSameOrigin = currentOrigin && normalizeBaseUrl(currentOrigin) === baseUrl;
const incomingCookieHeader = typeof params.req.headers.cookie === "string" ? params.req.headers.cookie : "";
const homeDiagnostics = await fetchHomeDiagnostics(baseUrl);
if (homeDiagnostics.ok) {
steps.push(
buildStaticStep({
id: "technical-performance-home",
label: "Performance de laccueil",
kind: "technical",
status: homeDiagnostics.durationMs <= 1500 ? "ok" : homeDiagnostics.durationMs <= 3000 ? "warning" : "error",
message:
homeDiagnostics.durationMs <= 1500
? "Temps de réponse confortable"
: homeDiagnostics.durationMs <= 3000
? "Temps de réponse correct mais à surveiller"
: "Temps de réponse trop élevé",
details: `${homeDiagnostics.durationMs} ms`,
critical: false,
durationMs: homeDiagnostics.durationMs,
}),
buildStaticStep({
id: "technical-security-baseline",
label: "Sécurité de base",
kind: "technical",
status: baseUrl.startsWith("https://") ? "ok" : "warning",
message: baseUrl.startsWith("https://") ? "Le site est servi en HTTPS" : "Le site nutilise pas HTTPS",
details: homeDiagnostics.finalUrl,
critical: false,
}),
buildStaticStep({
id: "technical-accessibility-baseline",
label: "Accessibilité de base",
kind: "technical",
status:
homeDiagnostics.body.includes("<title>")
&& /<html[^>]+lang=/i.test(homeDiagnostics.body)
? "ok"
: "warning",
message:
homeDiagnostics.body.includes("<title>")
&& /<html[^>]+lang=/i.test(homeDiagnostics.body)
? "Titre de page et langue du document détectés"
: "Titre ou langue du document à confirmer",
details: homeDiagnostics.finalUrl,
critical: false,
}),
buildStaticStep({
id: "technical-mobile-baseline",
label: "Compatibilité mobile",
kind: "technical",
status: /<meta[^>]+name=["']viewport["']/i.test(homeDiagnostics.body) ? "ok" : "warning",
message: /<meta[^>]+name=["']viewport["']/i.test(homeDiagnostics.body)
? "Viewport mobile détecté"
: "Meta viewport absente ou non détectée",
details: homeDiagnostics.finalUrl,
critical: false,
})
);
} else {
steps.push(
buildStaticStep({
id: "technical-performance-home",
label: "Performance de laccueil",
kind: "technical",
status: "error",
message: homeDiagnostics.error || "Impossible de mesurer laccueil",
details: homeDiagnostics.finalUrl,
critical: false,
durationMs: homeDiagnostics.durationMs,
})
);
}
steps.push(
await fetchJsonCheck({
id: "technical-api-public",
label: "API publique",
kind: "technical",
url: `${baseUrl}/api/trpc/auth.providers?input=%7B%7D`,
markers: ['"google"', '"facebook"'],
})
);
let adminCookieHeader: string | null = isSameOrigin && incomingCookieHeader ? incomingCookieHeader : null;
const hasConfiguredAdminAccount = Boolean(
target.adminEmail &&
(target.adminAuthMode === "oauth" || target.adminPassword)
);
if (!adminCookieHeader && hasConfiguredAdminAccount) {
try {
adminCookieHeader = await createConfiguredSessionCookie(target.adminEmail, target.adminAuthMode === "oauth" ? "" : target.adminPassword);
} catch (error) {
const usesOAuthOnly = target.adminAuthMode === "oauth";
steps.push({
id: "admin-config",
label: "Configuration compte admin de contrôle",
kind: "user",
status: "warning",
durationMs: 0,
message: usesOAuthOnly ? "Impossible douvrir une session admin OAuth" : "Impossible douvrir une session admin dédiée",
details: error instanceof Error ? error.message : usesOAuthOnly ? "Compte OAuth admin introuvable" : "Identifiants admin invalides",
critical: false,
});
}
}
let associationCookieHeader: string | null = null;
const hasConfiguredAssociationAccount = Boolean(
target.associationEmail &&
(target.associationAuthMode === "oauth" || target.associationPassword)
);
if (hasConfiguredAssociationAccount) {
try {
associationCookieHeader = await createConfiguredSessionCookie(
target.associationEmail,
target.associationAuthMode === "oauth" ? "" : target.associationPassword
);
} catch (error) {
const usesOAuthOnly = target.associationAuthMode === "oauth";
steps.push({
id: "association-config",
label: "Configuration compte association de test",
kind: "user",
status: "warning",
durationMs: 0,
message: usesOAuthOnly ? "Impossible douvrir une session association OAuth" : "Impossible douvrir une session association",
details: error instanceof Error ? error.message : usesOAuthOnly ? "Compte OAuth association introuvable" : "Identifiants association invalides",
critical: false,
});
}
} else {
steps.push({
id: "association-config",
label: "Configuration compte association de test",
kind: "user",
status: "warning",
durationMs: 0,
message: "Compte association non configuré",
details: "Les contrôles authentifiés association sont ignorés tant que lemail et le mot de passe ne sont pas renseignés.",
critical: false,
});
}
let accueilCookieHeader: string | null = null;
const hasConfiguredAccueilAccount = Boolean(
target.accueilEmail &&
(target.accueilAuthMode === "oauth" || target.accueilPassword)
);
if (hasConfiguredAccueilAccount) {
try {
accueilCookieHeader = await createConfiguredSessionCookie(
target.accueilEmail,
target.accueilAuthMode === "oauth" ? "" : target.accueilPassword
);
} catch (error) {
const usesOAuthOnly = target.accueilAuthMode === "oauth";
steps.push(buildStaticStep({
id: "accueil-config",
label: "Configuration compte accueil / hôtesse",
kind: "user",
status: "warning",
message: usesOAuthOnly ? "Impossible douvrir une session accueil OAuth" : "Impossible douvrir une session accueil",
details: error instanceof Error ? error.message : usesOAuthOnly ? "Compte OAuth accueil introuvable" : "Identifiants accueil invalides",
critical: false,
}));
}
} else {
steps.push(buildStaticStep({
id: "accueil-config",
label: "Configuration compte accueil / hôtesse",
kind: "user",
status: "warning",
message: "Compte accueil non configuré",
details: "Le parcours hôtesse reste ignoré tant quun compte accueil nest pas renseigné.",
critical: false,
}));
}
steps.push(
await fetchPageCheck({ id: "technical-public-home", label: "Navigation publique • Accueil", kind: "technical", url: `${baseUrl}/`, expectedPath: "/", markers: ["Portail Associations", "<title>Portail Associations</title>"] }),
await fetchPageCheck({ id: "technical-public-login", label: "Navigation publique • Connexion", kind: "technical", url: `${baseUrl}/login`, expectedPath: "/login", markers: ["Connexion", "Se connecter"] }),
await fetchPageCheck({ id: "technical-directory", label: "Annuaire public des associations", kind: "technical", url: `${baseUrl}/associations`, expectedPath: "/associations", markers: ["Annuaire des associations", "associationDirectory.listPortal"] }),
await fetchPageCheck({ id: "technical-map", label: "Cartographie des associations", kind: "technical", url: `${baseUrl}/associations/carte`, expectedPath: "/associations/carte", markers: ["Cartographie interactive", "associationDirectory.listMap"] }),
);
const publicProfileUrl = await tryFindPublicProfileUrl(baseUrl);
if (publicProfileUrl) {
steps.push(
await fetchPageCheck({
id: "technical-public-profile",
label: "Fiche publique association",
kind: "technical",
url: publicProfileUrl,
expectedPath: new URL(publicProfileUrl).pathname,
markers: ["Coordonnées", "actualités et publications"],
critical: false,
})
);
} else {
steps.push(buildStaticStep({
id: "technical-public-profile",
label: "Fiche publique association",
kind: "technical",
status: "warning",
durationMs: 0,
message: "Aucune fiche publique détectée",
details: "Le tour na pas trouvé de lien de fiche association dans lannuaire public.",
critical: false,
}));
}
if (associationCookieHeader) {
steps.push(
await fetchJsonCheck({
id: "technical-api-association",
label: "API association",
kind: "technical",
url: `${baseUrl}/api/trpc/auth.me?input=%7B%7D`,
cookieHeader: associationCookieHeader,
markers: ['"email"', '"role"'],
}),
await fetchPageCheck({ id: "user-association-dashboard", label: "Parcours association • Tableau de bord", kind: "user", url: `${baseUrl}/dashboard`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard", markers: ["Tableau de bord", "<title>Portail Associations</title>"] }),
await fetchPageCheck({ id: "user-association-requests", label: "Parcours association • Mes demandes", kind: "user", url: `${baseUrl}/dashboard/requests`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/requests", markers: ["Mes demandes"] }),
await fetchPageCheck({ id: "user-association-documents", label: "Parcours association • Mes documents", kind: "user", url: `${baseUrl}/dashboard/documents`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/documents", markers: ["Mes documents"] }),
await fetchPageCheck({ id: "user-association-directory", label: "Parcours association • Annuaire", kind: "user", url: `${baseUrl}/dashboard/associations`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/associations", markers: ["Annuaire des associations"], critical: false }),
await fetchPageCheck({ id: "user-association-form-salle", label: "Formulaire • Réservation de salle", kind: "user", url: `${baseUrl}/dashboard/reservation-salle`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/reservation-salle", markers: ["Réservation de salle", "Soumettre"] }),
await fetchPageCheck({ id: "user-association-form-request", label: "Formulaire • Nouvelle demande", kind: "user", url: `${baseUrl}/dashboard/requests/new`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/requests/new", markers: ["Nouvelle demande", "Soumettre"] }),
await fetchPageCheck({ id: "user-association-form-material", label: "Formulaire • Matériel événementiel", kind: "user", url: `${baseUrl}/dashboard/materiel-evenementiel`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/materiel-evenementiel", markers: ["matériel événementiel", "Soumettre"], critical: false }),
);
}
if (accueilCookieHeader) {
steps.push(
await fetchJsonCheck({
id: "technical-api-accueil",
label: "API accueil / hôtesse",
kind: "technical",
url: `${baseUrl}/api/trpc/auth.me?input=%7B%7D`,
cookieHeader: accueilCookieHeader,
markers: ['"accueil"'],
critical: false,
}),
await fetchPageCheck({ id: "user-accueil-dashboard", label: "Parcours hôtesse • Administration", kind: "user", url: `${baseUrl}/admin`, cookieHeader: accueilCookieHeader, expectedPath: "/admin", markers: ["Administration accueil", "<title>Portail Associations</title>"] }),
await fetchPageCheck({ id: "user-accueil-requests", label: "Parcours hôtesse • Gestion des demandes", kind: "user", url: `${baseUrl}/admin?tab=requests`, cookieHeader: accueilCookieHeader, expectedPath: "/admin", markers: ["Gestion des demandes"] }),
await fetchPageCheck({ id: "user-accueil-calendar", label: "Parcours hôtesse • Calendrier", kind: "user", url: `${baseUrl}/admin?tab=calendrier`, cookieHeader: accueilCookieHeader, expectedPath: "/admin", markers: ["Calendrier"] }),
await fetchPageCheck({ id: "user-accueil-notifications", label: "Parcours hôtesse • Notifications", kind: "user", url: `${baseUrl}/admin?tab=notifications`, cookieHeader: accueilCookieHeader, expectedPath: "/admin", markers: ["Notifications"], critical: false }),
);
}
if (adminCookieHeader) {
steps.push(
await fetchJsonCheck({
id: "technical-api-admin",
label: "API administration",
kind: "technical",
url: `${baseUrl}/api/internal/stats/reservations?period=month`,
cookieHeader: adminCookieHeader,
markers: ['"summary"', '"history"'],
}),
await fetchPageCheck({ id: "user-admin-dashboard", label: "Parcours administrateur • Administration", kind: "user", url: `${baseUrl}/admin`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Tableau de bord", "<title>Portail Associations</title>"] }),
await fetchPageCheck({ id: "user-admin-requests", label: "Parcours administrateur • Gestion des demandes", kind: "user", url: `${baseUrl}/admin?tab=requests`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Gestion des demandes"] }),
await fetchPageCheck({ id: "user-admin-calendar", label: "Parcours administrateur • Calendrier", kind: "user", url: `${baseUrl}/admin?tab=calendrier`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Calendrier"] }),
await fetchPageCheck({ id: "user-admin-analytics", label: "Parcours administrateur • Statistiques", kind: "user", url: `${baseUrl}/admin?tab=analytics`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Statistiques dactivité", "Tour opérationnel du site"] }),
await fetchPageCheck({ id: "user-admin-ccds-directory", label: "Parcours administrateur • Annuaire interne CCDS", kind: "user", url: `${baseUrl}/admin?tab=ccds-directory`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Annuaire interne CCDS"] }),
);
} else {
steps.push(buildStaticStep({
id: "admin-config",
label: "Configuration session admin",
kind: "user",
status: "warning",
durationMs: 0,
message: "Aucune session admin disponible pour cet environnement",
details: isSameOrigin
? "Le navigateur na pas transmis de cookie de session exploitable."
: "Renseignez un compte admin dédié si vous voulez tester un autre environnement que celui où vous êtes connecté.",
critical: false,
}));
}
const finishedAt = new Date();
const report: OperationalTourReport = {
id: `tour-${finishedAt.getTime()}`,
environment: params.environment,
environmentLabel,
baseUrl,
status: computeReportStatus(steps),
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
durationMs: finishedAt.getTime() - startedAt.getTime(),
initiatedBy: {
userId: params.currentUser.id,
name: params.currentUser.name || params.currentUser.email || `Utilisateur #${params.currentUser.id}`,
email: params.currentUser.email || undefined,
},
summary: summarizeSteps(steps),
steps,
};
const nextHistory = [report, ...(await getOperationalTourHistory())];
await saveOperationalTourHistory(nextHistory);
return report;
}