repos
/ status-rust master

status-rust

mirror archived upstream

Single-binary self-hosted uptime monitoring and status pages on Rust axum: HTTP probes, Lighthouse audits, SEO crawler, and PDF reports.

axumdockerrustself-hostedsqlitestatus-pageuptime-monitoringvite

3.7 KB · 123 lines · Rust Raw History
  1use axum::{
  2    extract::{Form, Path as AxumPath, Query, State},
  3    http::StatusCode,
  4    response::{IntoResponse, Json, Redirect, Response},
  5    routing::{get, post},
  6    Router,
  7};
  8use serde::Deserialize;
  9use serde_json::{json, Value};
 10use tower_cookies::Cookies;
 11use uuid::Uuid;
 12
 13use crate::models;
 14use crate::render::render;
 15use crate::routes::auth::is_authenticated;
 16use crate::routes::dashboard::build_property_context;
 17use crate::AppState;
 18
 19pub fn router() -> Router<AppState> {
 20    Router::new()
 21        .route("/properties", get(properties).post(properties_create))
 22        .route("/properties/{id}/delete", post(property_delete))
 23        .route("/properties/{id}/public", post(property_public_toggle))
 24}
 25
 26#[derive(Debug, Deserialize)]
 27pub struct ListQuery {
 28    pub q: Option<String>,
 29}
 30
 31#[derive(Debug, Deserialize)]
 32pub struct CreateForm {
 33    pub url: String,
 34}
 35
 36pub async fn properties(
 37    State(state): State<AppState>,
 38    cookies: Cookies,
 39    Query(query): Query<ListQuery>,
 40) -> Response {
 41    if !is_authenticated(&cookies, &state) {
 42        return Redirect::to("/").into_response();
 43    }
 44    let search = query.q.as_deref().filter(|s| !s.is_empty());
 45    let rows = models::list_properties(&state.pool, search)
 46        .await
 47        .unwrap_or_default();
 48    let mut props: Vec<Value> = Vec::with_capacity(rows.len());
 49    for row in &rows {
 50        match build_property_context(&state, row).await {
 51            Ok(ctx) => props.push(serde_json::to_value(&ctx).unwrap_or(Value::Null)),
 52            Err(e) => tracing::warn!("[properties] property context: {e:#}"),
 53        }
 54    }
 55    let extra = minijinja::context! {
 56        page => minijinja::context! {
 57            title => "Properties",
 58            description => "Manage your properties.",
 59        },
 60        title => "Properties",
 61        description => "Manage your properties.",
 62        properties => props,
 63        q => query.q,
 64    };
 65    render(&state, "properties/properties.html", "/properties", true, extra)
 66}
 67
 68pub async fn properties_create(
 69    State(state): State<AppState>,
 70    cookies: Cookies,
 71    Form(form): Form<CreateForm>,
 72) -> Response {
 73    if !is_authenticated(&cookies, &state) {
 74        return Redirect::to("/").into_response();
 75    }
 76    let url = form.url.trim();
 77    // https only: the checker is HTTP/2-over-TLS only and rejects plain
 78    // http:// probes, so accepting one here would create a property that can
 79    // never pass a check.
 80    if url.is_empty() || !url.starts_with("https://") {
 81        return Redirect::to("/properties").into_response();
 82    }
 83    if let Err(e) = models::create_property(&state.pool, url).await {
 84        tracing::warn!("[properties] create: {e:#}");
 85    }
 86    Redirect::to("/properties").into_response()
 87}
 88
 89pub async fn property_delete(
 90    State(state): State<AppState>,
 91    cookies: Cookies,
 92    AxumPath(id): AxumPath<Uuid>,
 93) -> Response {
 94    if !is_authenticated(&cookies, &state) {
 95        return Redirect::to("/").into_response();
 96    }
 97    if let Err(e) = models::delete_property(&state.pool, id).await {
 98        tracing::warn!("[properties] delete: {e:#}");
 99    }
100    Redirect::to("/properties").into_response()
101}
102
103pub async fn property_public_toggle(
104    State(state): State<AppState>,
105    cookies: Cookies,
106    AxumPath(id): AxumPath<Uuid>,
107) -> Response {
108    if !is_authenticated(&cookies, &state) {
109        return Redirect::to("/").into_response();
110    }
111    match models::toggle_public(&state.pool, id).await {
112        Ok(is_public) => Json(json!({"success": true, "is_public": is_public})).into_response(),
113        Err(e) => {
114            tracing::warn!("[properties] toggle public: {e:#}");
115            (
116                StatusCode::INTERNAL_SERVER_ERROR,
117                Json(json!({"success": false})),
118            )
119                .into_response()
120        }
121    }
122}