203 lines
9.7 KiB
Rust
Executable File
203 lines
9.7 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 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<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 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::<SocketAddr>())
|
|
.with_graceful_shutdown(async {
|
|
tokio::signal::ctrl_c().await.ok();
|
|
})
|
|
.await?;
|
|
Ok(())
|
|
}
|