ITSM/src/main.rs

154 lines
6.5 KiB
Rust
Executable File

//! 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<AppState>) -> 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::<SocketAddr>())
.with_graceful_shutdown(async {
tokio::signal::ctrl_c().await.ok();
})
.await?;
Ok(())
}