perfect-postcode/server-rs/src/routes/shorten.rs
2026-03-17 21:08:32 +00:00

165 lines
4.5 KiB
Rust

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<Arc<SharedState>>, Json(req): Json<ShortenRequest>) -> 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<Arc<SharedState>>, Path(code): Path<String>) -> 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()
}
}
}