use std::sync::Arc; use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Redirect, Response}; use axum::Json; use rand::Rng; use serde::{Deserialize, Serialize}; use tracing::warn; use crate::pocketbase::auth_superuser; 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 auth_superuser( &state.http_client, pb_url, &state.pocketbase_admin_email, &state.pocketbase_admin_password, ) .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 auth_superuser( &state.http_client, pb_url, &state.pocketbase_admin_email, &state.pocketbase_admin_password, ) .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) => { Redirect::temporary(&format!("/dashboard?{params}")).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() } } }