From 4e4ac3033272f7396fa5b219e29b82765583ad0d Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Thu, 5 Feb 2026 21:58:57 +0000 Subject: [PATCH] refactor(account): convert token creation from imperative JS to Datastar Replace fetch/classList DOM manipulation with Datastar signals and @post. Server now returns signal patches for Datastar requests while preserving the JSON API for CLI usage. --- src/application/routes/api/tokens.rs | 42 ++++++--- templates/pages/account.html | 128 +++++++++++---------------- 2 files changed, 84 insertions(+), 86 deletions(-) diff --git a/src/application/routes/api/tokens.rs b/src/application/routes/api/tokens.rs index 5d62b30..ac6f346 100644 --- a/src/application/routes/api/tokens.rs +++ b/src/application/routes/api/tokens.rs @@ -1,11 +1,16 @@ use axum::Json; use axum::extract::{Path, State}; -use axum::http::StatusCode; +use axum::http::{HeaderMap, StatusCode}; +use axum::response::{IntoResponse, Response}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use tracing::{error, info}; use crate::application::auth::AuthenticatedUser; +use crate::application::errors::{ApiError, AppError}; +use crate::application::routes::support::{ + FlexiblePayload, is_datastar_request, render_signals_json, +}; use crate::application::server::AppState; use crate::domain::ids::{TokenId, UserId}; use crate::domain::tokens::{NewToken, Token}; @@ -46,15 +51,18 @@ impl From for TokenResponse { } } -#[tracing::instrument(skip(state, auth_user, payload), fields(token_name = %payload.name))] +#[tracing::instrument(skip(state, auth_user, headers, payload))] pub async fn create_token( State(state): State, auth_user: AuthenticatedUser, - Json(payload): Json, -) -> Result, StatusCode> { + headers: HeaderMap, + payload: FlexiblePayload, +) -> Result { + let (payload, _source) = payload.into_parts(); + let token_value = generate_token().map_err(|err| { error!(error = %err, "failed to generate token"); - StatusCode::INTERNAL_SERVER_ERROR + ApiError::from(AppError::unexpected("failed to generate token")) })?; let token_hash_value = hash_token(&token_value); @@ -62,16 +70,28 @@ pub async fn create_token( let stored_token = state.token_repo.insert(new_token).await.map_err(|err| { error!(error = %err, "failed to store token"); - StatusCode::INTERNAL_SERVER_ERROR + ApiError::from(AppError::unexpected("failed to store token")) })?; info!(token_id = %stored_token.id, token_name = %stored_token.name, user_id = %auth_user.0.id, "API token created"); - Ok(Json(CreateTokenResponse { - id: stored_token.id, - name: stored_token.name, - token: token_value, - })) + if is_datastar_request(&headers) { + use serde_json::Value; + let signals = vec![ + ("_token-value", Value::String(token_value)), + ("_token-created", Value::Bool(true)), + ("_creating-token", Value::Bool(false)), + ("_show-token-form", Value::Bool(false)), + ]; + render_signals_json(&signals).map_err(ApiError::from) + } else { + Ok(Json(CreateTokenResponse { + id: stored_token.id, + name: stored_token.name, + token: token_value, + }) + .into_response()) + } } #[tracing::instrument(skip(state, auth_user))] diff --git a/templates/pages/account.html b/templates/pages/account.html index e4eb2d4..278bcad 100644 --- a/templates/pages/account.html +++ b/templates/pages/account.html @@ -90,7 +90,13 @@ -
+

API Tokens

{% if tokens.is_empty() %} @@ -125,44 +131,53 @@ {% endif %} - -