use std::sync::Arc; use axum::extract::{Path, State}; use axum::http::{header, StatusCode}; use axum::response::{Html, IntoResponse, Response}; use axum::Json; use rand::Rng; use serde::{Deserialize, Serialize}; use tracing::warn; use crate::pocketbase::get_superuser_token; use crate::state::SharedState; const CODE_LEN: usize = 8; const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789"; fn generate_code() -> String { let mut rng = rand::rng(); (0..CODE_LEN) .map(|_| CHARSET[rng.random_range(0..CHARSET.len())] as char) .collect() } #[derive(Deserialize)] pub struct ShortenRequest { params: String, } #[derive(Serialize)] pub struct ShortenResponse { code: String, url: String, } #[derive(Serialize)] struct PbRecord { code: String, params: String, } pub async fn post_shorten( State(shared): State>, Json(req): Json, ) -> Response { let state = shared.load_state(); let pb_url = state.pocketbase_url.trim_end_matches('/'); let token = match get_superuser_token(&state).await { Ok(t) => t, Err(err) => { warn!("PocketBase superuser auth failed: {err}"); return StatusCode::BAD_GATEWAY.into_response(); } }; let code = generate_code(); let record = PbRecord { code: code.clone(), params: req.params, }; let res = state .http_client .post(format!("{pb_url}/api/collections/short_urls/records")) .header("Authorization", format!("Bearer {token}")) .json(&record) .send() .await; match res { Ok(resp) if resp.status().is_success() => { let body = ShortenResponse { url: format!("/s/{code}"), code, }; Json(body).into_response() } Ok(resp) => { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); warn!("PocketBase create failed ({status}): {text}"); StatusCode::BAD_GATEWAY.into_response() } Err(err) => { warn!("PocketBase request error: {err}"); StatusCode::BAD_GATEWAY.into_response() } } } pub async fn get_short_url( State(shared): State>, Path(code): Path, ) -> Response { let state = shared.load_state(); if code.is_empty() || code.len() > 20 || !code.bytes().all(|b| b.is_ascii_alphanumeric()) { return StatusCode::BAD_REQUEST.into_response(); } let pb_url = state.pocketbase_url.trim_end_matches('/'); let token = match get_superuser_token(&state).await { Ok(t) => t, Err(err) => { warn!("PocketBase superuser auth failed: {err}"); return StatusCode::BAD_GATEWAY.into_response(); } }; let filter = format!("code=\"{code}\""); let url = format!( "{pb_url}/api/collections/short_urls/records?filter={}&perPage=1", urlencoding::encode(&filter) ); let res = state .http_client .get(&url) .header("Authorization", format!("Bearer {token}")) .send() .await; match res { Ok(resp) if resp.status().is_success() => { let json: serde_json::Value = match resp.json().await { Ok(v) => v, Err(err) => { warn!("Failed to parse PocketBase response: {err}"); return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } }; let params = json["items"] .as_array() .and_then(|items| items.first()) .and_then(|item| item["params"].as_str()); match params { Some(params) => { let redirect_url = format!("/dashboard?{params}"); let og_image_url = format!("{}/api/screenshot?og=1&{params}", state.public_url); let og_url = format!("{}/s/{code}", state.public_url); let og_title = "Perfect Postcode | Every neighbourhood in England"; let og_description = "Explore property prices, energy ratings, crime stats, school ratings, and more across England on one interactive map."; let html = format!( r#" {og_title} "# ); ( [(header::CACHE_CONTROL, "public, max-age=86400")], Html(html), ) .into_response() } None => StatusCode::NOT_FOUND.into_response(), } } Ok(resp) => { let status = resp.status(); warn!("PocketBase lookup failed ({status})"); StatusCode::BAD_GATEWAY.into_response() } Err(err) => { warn!("PocketBase request error: {err}"); StatusCode::BAD_GATEWAY.into_response() } } }