//! 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 dashboard; mod db; 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 state = AppState { cfg: cfg.clone(), db: db.clone(), forge, 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)) // 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)) // 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(()) }