use axum::{
extract::{Form, Path, State},
http::{HeaderMap, StatusCode},
response::{Html, Redirect},
};
use minijinja::context;
use qrcode::render::svg;
use qrcode::QrCode;
use serde::Deserialize;
use tracing::error;
use crate::auth;
use crate::middleware::UserContext;
use crate::AppState;
#[derive(Debug, Deserialize)]
pub struct CreateTokenForm {
pub label: Option<String>,
pub days: Option<i64>,
}
pub async fn show_api_tokens(
State(state): State<AppState>,
user: UserContext,
) -> Result<Html<String>, StatusCode> {
let tokens = auth::list_api_tokens(state.db.as_ref(), user.user.id)
.await
.map_err(|e| {
error!(error = %e, "list api tokens failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let template = state.env.get_template("api_tokens/list.html")
.map_err(|e| { error!(error = %e, "load api_tokens/list.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
let rendered = template
.render(context! {
title => "API tokens",
user => user.user,
tokens => tokens,
now => chrono::Utc::now(),
})
.map_err(|e| { error!(error = %e, "render api_tokens/list.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
Ok(Html(rendered))
}
pub async fn create_api_token(
State(state): State<AppState>,
user: UserContext,
headers: HeaderMap,
Form(form): Form<CreateTokenForm>,
) -> Result<Html<String>, StatusCode> {
let label = form.label.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
let days = form.days.filter(|d| *d > 0);
let token = auth::create_api_token(state.db.as_ref(), user.user.id, label, days)
.await
.map_err(|e| {
error!(error = %e, "create api token failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let base = derive_base_url(&headers, &state.config.bind_address);
let pair_uri = format!(
"dms://pair?base={}&token={}",
urlencoded(&base),
urlencoded(&token.token),
);
let qr_svg = render_qr_svg(&pair_uri).unwrap_or_else(|e| {
error!(error = %e, "QR render failed");
String::new()
});
let template = state.env.get_template("api_tokens/created.html")
.map_err(|e| { error!(error = %e, "load api_tokens/created.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
let rendered = template
.render(context! {
title => "API token created",
user => user.user,
token => token,
pair_uri => pair_uri,
base_url => base,
qr_svg => qr_svg,
})
.map_err(|e| { error!(error = %e, "render api_tokens/created.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
Ok(Html(rendered))
}
pub async fn delete_api_token(
State(state): State<AppState>,
user: UserContext,
Path(id): Path<i64>,
) -> Result<Redirect, StatusCode> {
auth::delete_api_token(state.db.as_ref(), user.user.id, id)
.await
.map_err(|e| {
error!(error = %e, "delete api token failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Redirect::to("/api-tokens"))
}
/// Pick the public-facing base URL the Android app should hit, in priority:
/// 1. `DMS_PUBLIC_URL` env (explicit override for reverse-proxy setups)
/// 2. `X-Forwarded-Proto` + `X-Forwarded-Host` (standard proxy headers)
/// 3. The request's `Host` header (covers the common case of LAN access —
/// visiting `http://192.168.1.50:3007/api-tokens` makes the QR encode
/// that exact origin so the phone can dial it back)
/// 4. Bind address as a last resort (will be `0.0.0.0:…` and unusable, but
/// at least there's something in the QR for the user to inspect)
fn derive_base_url(headers: &HeaderMap, bind_address: &str) -> String {
if let Ok(v) = std::env::var("DMS_PUBLIC_URL") {
return v.trim_end_matches('/').to_string();
}
let scheme = headers
.get("x-forwarded-proto")
.and_then(|v| v.to_str().ok())
.map(|s| s.split(',').next().unwrap_or(s).trim().to_string())
.unwrap_or_else(|| "http".to_string());
let host = headers
.get("x-forwarded-host")
.or_else(|| headers.get(axum::http::header::HOST))
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
match host {
Some(h) => format!("{scheme}://{h}").trim_end_matches('/').to_string(),
None => format!("http://{bind_address}"),
}
}
fn render_qr_svg(payload: &str) -> Result<String, qrcode::types::QrError> {
let code = QrCode::new(payload.as_bytes())?;
Ok(code
.render::<svg::Color>()
.min_dimensions(280, 280)
.quiet_zone(true)
.build())
}
fn urlencoded(s: &str) -> String {
s.bytes()
.map(|b| match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
(b as char).to_string()
}
_ => format!("%{:02X}", b),
})
.collect()
}
⭐
Log in to mark this repository.
5.0 KiB · text
History
1ce9641