//! ITSM-Plattform -- eigenstaendiges Produkt (nicht Teil von AES), mandantenfaehig. //! //! Rust-Rewrite 2026-07-15 (zuvor Python/Flask). Betrieb gemaess //! ISO 27001 / DSGVO / NIS 2: //! - PostgreSQL-Persistenz, Audit-Log fuer alle aendernden Aktionen //! - serverseitige Sessions (widerrufbar), CSRF-Schutz, Login-Rate-Limit, //! Argon2id-Passwoerter (Werkzeug-Altbestand wird beim Login migriert) //! - automatische Aufbewahrungsfrist-Bereinigung (DSGVO Speicherbegrenzung) //! - ITIL-v3/v4-Prozesse: Statusmodell, Impact/Urgency-Matrix, Change //! Enablement mit Freigabe, Problem Management, SLA, Knowledge-Freigabe //! //! AES bleibt ein buchbarer Service im Katalog, integriert per HTTP. //! Repo-Bearbeitung aus Tickets (phase-008): nur admin/change_manager, nur aus //! freigegebenen Change-Tickets, jede Aenderung zwingend im Worklog. mod admin; mod auth; mod cmdb; mod config; mod crm; mod dashboard; mod hr; mod db; mod extensions; mod forge; mod itil; mod kb; mod security; mod services; mod tickets; mod web; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use axum::extract::State; use axum::response::IntoResponse; use axum::routing::{get, post}; use axum::{Json, Router}; use tokio::sync::Mutex; use tower_http::services::ServeDir; use web::AppState; async fn health(State(app): State) -> impl IntoResponse { match app.db.health().await { Ok(()) => Json(serde_json::json!({"status": "ok", "db": "ok"})).into_response(), Err(e) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"status": "error", "db": e.to_string()}))).into_response(), } } #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt().with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info".into())).init(); let cfg = Arc::new(config::Config::from_env()?); let db = db::Db::connect(&cfg.database_url)?; db.init_schema().await?; tracing::info!("Schema initialisiert."); let forge = Arc::new(forge::ForgeClient::from_config(&cfg.forge_base_url, &cfg.forge_service_token)); if forge.is_none() { tracing::info!("FORGE_BASE_URL nicht gesetzt -- Repo-Bearbeitung aus Tickets deaktiviert."); } let aes = Arc::new(extensions::AesClient::from_config(&cfg)); if aes.is_none() { tracing::info!("ITSM_AES_URL nicht gesetzt -- AES-Suite-Erweiterung deaktiviert."); } let state = AppState { cfg: cfg.clone(), db: db.clone(), forge, aes, http: reqwest::Client::builder() .timeout(std::time::Duration::from_secs(5)) .build()?, svc_status_cache: Arc::new(Mutex::new(HashMap::new())), }; // Retention-Bereinigung im Hintergrund (Advisory-Lock in der DB verhindert // parallele Laeufe mehrerer Instanzen). { let db = db.clone(); let interval = cfg.retention_interval_seconds; tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop { if let Err(e) = db.run_retention_cleanup().await { tracing::error!("Retention-Bereinigung fehlgeschlagen: {:#}", e); } tokio::time::sleep(std::time::Duration::from_secs(interval)).await; } }); } let app = Router::new() .route("/", get(auth::root)) .route("/health", get(health)) .route("/login", get(auth::login_get).post(auth::login_post)) .route("/logout", post(auth::logout)) .route("/setup/new", get(auth::setup_get).post(auth::setup_post)) .route("/dashboard", get(dashboard::dashboard)) // Tickets + ITIL-Prozesse .route("/tickets", get(tickets::tickets_list)) .route("/probleme", get(tickets::probleme_list)) .route("/aenderungen", get(tickets::aenderungen_list)) .route("/releases", get(tickets::releases_list)) .route("/tickets/board", get(tickets::board)) .route("/tickets/new", post(tickets::ticket_new)) .route("/tickets/:id/status", post(tickets::ticket_status)) .route("/tickets/:id/comment", post(tickets::ticket_comment)) .route("/tickets/:id/approval", post(tickets::ticket_approval)) .route("/tickets/:id/problem-link", post(tickets::ticket_problem_link)) .route("/tickets/:id/known-error", post(tickets::ticket_known_error)) .route("/tickets/:id/ci-link", post(tickets::ticket_ci_link)) .route("/tickets/:id/ci-unlink", post(tickets::ticket_ci_unlink)) .route("/tickets/:id/repo-edit", post(tickets::ticket_repo_edit)) .route("/api/tickets/:id", get(tickets::api_ticket)) .route("/api/tickets/:id/repo-file", get(tickets::api_repo_file)) // Modul HR (Konzernplattform phase-001, nur admin + hr_manager) .route("/hr", get(hr::hr_list)) .route("/hr/neu", post(hr::hr_new)) .route("/hr/:id", get(hr::hr_detail).post(hr::hr_update)) .route("/hr/:id/abwesenheiten", post(hr::hr_absence_new)) .route("/hr/abwesenheiten/:id/status", post(hr::hr_absence_status)) // Modul CRM (Konzernplattform phase-001, nur admin + crm_agent) .route("/crm", get(crm::crm_list)) .route("/crm/neu", post(crm::crm_new)) .route("/crm/pipeline", get(crm::crm_pipeline)) .route("/crm/:id", get(crm::crm_detail).post(crm::crm_update)) .route("/crm/:id/kontakte", post(crm::crm_contact_new)) .route("/crm/:id/deals", post(crm::crm_deal_new)) .route("/crm/deals/:id/stufe", post(crm::crm_deal_stage)) // Service-Katalog .route("/services", get(services::services_page)) .route("/services/new", post(services::service_new)) // Wissensdatenbank .route("/wissen", get(kb::kb_list)) .route("/wissen/new", get(kb::kb_new_get).post(kb::kb_new_post)) .route("/wissen/:id", get(kb::kb_detail)) .route("/wissen/:id/edit", get(kb::kb_edit_get).post(kb::kb_edit_post)) .route("/wissen/:id/status", post(kb::kb_status_post)) .route("/wissen/:id/delete", post(kb::kb_delete)) // CMDB .route("/assets", get(cmdb::cmdb_list)) .route("/assets/new", get(cmdb::ci_new_get).post(cmdb::ci_new_post)) .route("/assets/:id", get(cmdb::ci_detail_get).post(cmdb::ci_detail_post)) .route("/assets/:id/relationships", post(cmdb::ci_rel_add)) .route("/assets/:id/relationships/:rel_id/delete", post(cmdb::ci_rel_delete)) // Erweiterungen -> AES-Suite Dashboard (admin + Rolle aes_user) .route("/erweiterungen/aes", get(extensions::aes_dashboard)) .route("/erweiterungen/aes/projekte", get(extensions::aes_projekte).post(extensions::aes_project_create)) .route("/erweiterungen/aes/projekte/:name", get(extensions::aes_project_detail)) .route("/erweiterungen/aes/projekte/:name/phasen/:phase/run", post(extensions::aes_phase_run)) .route("/erweiterungen/aes/projekte/:name/phasen/:phase/validate", post(extensions::aes_phase_validate)) .route("/erweiterungen/aes/forge", get(extensions::aes_forge).post(extensions::aes_forge_create)) .route("/erweiterungen/aes/forge/:name", get(extensions::aes_forge_repo)) .route("/erweiterungen/aes/forge/:name/delete", post(extensions::aes_forge_delete)) .route("/erweiterungen/aes/forge/:name/pulls", post(extensions::aes_forge_pull_create)) .route("/erweiterungen/aes/forge/:name/pulls/:number/merge", post(extensions::aes_forge_pull_merge)) .route("/erweiterungen/aes/runner", get(extensions::aes_runner)) .route("/erweiterungen/aes/llm", get(extensions::aes_llm)) .route("/erweiterungen/aes/llm/infer", post(extensions::aes_llm_infer)) .route("/erweiterungen/aes/llm/pull", post(extensions::aes_llm_pull)) .route("/erweiterungen/aes/llm/delete", post(extensions::aes_llm_delete)) .route("/erweiterungen/aes/llm/connections", post(extensions::aes_llm_conn_save)) .route("/erweiterungen/aes/agenten", get(extensions::aes_agenten)) .route("/erweiterungen/aes/monitoring", get(extensions::aes_monitoring)) .route("/erweiterungen/aes/monitor.json", get(extensions::aes_monitor_json)) .route("/erweiterungen/aes/admin", get(extensions::aes_admin)) .route("/erweiterungen/aes/einstellungen", get(extensions::aes_einstellungen)) .route("/erweiterungen/aes/einstellungen/save", post(extensions::aes_einstellungen_save)) // Externe Metrics-Schnittstelle (Token-geschuetzt, kein Session-Login) .route("/aes/metrics", get(extensions::aes_metrics)) .route("/aes/metrics.json", get(extensions::aes_metrics_json)) // Administration .route("/admin", get(admin::settings_get).post(admin::settings_post)) .route("/admin/retention/run", post(admin::retention_run)) .route("/admin/audit", get(admin::audit_page)) .route("/admin/users", get(admin::users_get).post(admin::users_post)) .route("/admin/users/:id/edit", get(admin::user_edit_get).post(admin::user_edit_post)) .route("/admin/users/:id/role", post(admin::user_role_post)) .route("/admin/users/:id/active", post(admin::user_active_post)) .nest_service("/static", ServeDir::new("static")) .layer(axum::middleware::from_fn_with_state(state.clone(), web::ctx_middleware)) .with_state(state); let listener = tokio::net::TcpListener::bind(&cfg.bind).await?; tracing::info!("ITSM laeuft auf http://{}", cfg.bind); axum::serve(listener, app.into_make_service_with_connect_info::()) .with_graceful_shutdown(async { tokio::signal::ctrl_c().await.ok(); }) .await?; Ok(()) }