repos
/ finance-rust master

finance-rust

mirror archived upstream

Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.

axumdockerfinancerustself-hostedsqlitestocksvite

3.6 KB · 118 lines · Rust Raw History
  1//! `POST /api/watchlist` (add) and `POST /api/watchlist/remove` — edit the
  2//! current browser session's dashboard watchlist (Phase C).
  3//!
  4//! Both resolve the session from the `fin_sid` cookie (minting one on a first
  5//! visit), so they work even if a script calls them before the dashboard set
  6//! the cookie. Add ensures the symbol is in the universe first (validating /
  7//! backfilling a brand-new ticker via `ensure_symbol`), then appends it.
  8
  9use axum::{
 10    extract::State,
 11    http::{header, HeaderMap, StatusCode},
 12    response::{IntoResponse, Response},
 13    routing::post,
 14    Json, Router,
 15};
 16use serde::{Deserialize, Serialize};
 17
 18use crate::routes::symbols::ensure_symbol;
 19use crate::{watchlist, AppState};
 20
 21pub fn router() -> Router<AppState> {
 22    Router::new()
 23        .route("/api/watchlist", post(add))
 24        .route("/api/watchlist/remove", post(remove))
 25}
 26
 27#[derive(Deserialize)]
 28struct Body {
 29    ticker: String,
 30}
 31
 32#[derive(Serialize)]
 33struct Resp {
 34    ok: bool,
 35    #[serde(skip_serializing_if = "Option::is_none")]
 36    ticker: Option<String>,
 37    #[serde(skip_serializing_if = "Option::is_none")]
 38    name: Option<String>,
 39    #[serde(skip_serializing_if = "Option::is_none")]
 40    error: Option<String>,
 41}
 42
 43/// Add a symbol to the session's watchlist, validating / backfilling it into
 44/// the universe first if it is not tracked yet.
 45async fn add(State(state): State<AppState>, headers: HeaderMap, Json(body): Json<Body>) -> Response {
 46    let session = watchlist::resolve(&state.pool, &headers).await;
 47    match ensure_symbol(&state, &body.ticker).await {
 48        Ok(o) => {
 49            if let Err(e) = watchlist::add_ticker(&state.pool, &session.sid, &o.ticker).await {
 50                tracing::warn!("watchlist add {}: {e:#}", o.ticker);
 51                return reply(
 52                    &session,
 53                    StatusCode::INTERNAL_SERVER_ERROR,
 54                    Resp {
 55                        ok: false,
 56                        ticker: None,
 57                        name: None,
 58                        error: Some("Could not save to your watchlist.".into()),
 59                    },
 60                );
 61            }
 62            reply(
 63                &session,
 64                StatusCode::OK,
 65                Resp {
 66                    ok: true,
 67                    ticker: Some(o.ticker),
 68                    name: Some(o.name),
 69                    error: None,
 70                },
 71            )
 72        }
 73        Err((status, msg)) => reply(
 74            &session,
 75            status,
 76            Resp {
 77                ok: false,
 78                ticker: None,
 79                name: None,
 80                error: Some(msg),
 81            },
 82        ),
 83    }
 84}
 85
 86/// Remove a symbol from the session's watchlist. Idempotent.
 87async fn remove(
 88    State(state): State<AppState>,
 89    headers: HeaderMap,
 90    Json(body): Json<Body>,
 91) -> Response {
 92    let session = watchlist::resolve(&state.pool, &headers).await;
 93    let ticker = body.ticker.trim().to_uppercase();
 94    let _ = watchlist::remove_ticker(&state.pool, &session.sid, &ticker).await;
 95    reply(
 96        &session,
 97        StatusCode::OK,
 98        Resp {
 99            ok: true,
100            ticker: Some(ticker),
101            name: None,
102            error: None,
103        },
104    )
105}
106
107/// Build the JSON response, attaching the `Set-Cookie` header when the session
108/// was just minted.
109fn reply(session: &watchlist::Session, status: StatusCode, body: Resp) -> Response {
110    let mut resp = (status, Json(body)).into_response();
111    if let Some(c) = &session.set_cookie {
112        if let Ok(v) = header::HeaderValue::from_str(c) {
113            resp.headers_mut().insert(header::SET_COOKIE, v);
114        }
115    }
116    resp
117}