Merge pull request #36 from stevedylandev/feat/cellar-add-api f8b2bd6d
feat: add cellar api
Steve Simkins · 2026-04-24 21:49 4 file(s) · +40 −2
Cargo.lock +2 −1
695 695
696 696
[[package]]
697 697
name = "cellar"
698 -
version = "0.2.1"
698 +
version = "0.2.2"
699 699
dependencies = [
700 700
 "andromeda-auth",
701 701
 "andromeda-darkmatter-css",
717 717
 "serde_rusqlite",
718 718
 "subtle",
719 719
 "tokio",
720 +
 "tower-http",
720 721
 "tracing",
721 722
 "tracing-subscriber",
722 723
]
apps/cellar/Cargo.toml +2 −1
1 1
[package]
2 2
name = "cellar"
3 -
version = "0.2.1"
3 +
version = "0.2.2"
4 4
edition = "2024"
5 5
description = "Personal wine tasting log"
6 6
license = "MIT"
30 30
image = "0.25"
31 31
serde_rusqlite = "0.41"
32 32
chrono = "0.4"
33 +
tower-http = { workspace = true, features = ["cors"] }
apps/cellar/src/server/handlers/public.rs +25 −0
1 1
use askama_web::WebTemplate;
2 2
use axum::{
3 +
    Json,
3 4
    extract::{Path, State},
4 5
    http::{HeaderValue, StatusCode},
5 6
    response::{Html, IntoResponse, Response},
79 80
        Err(e) => {
80 81
            tracing::error!("Failed to get wine: {}", e);
81 82
            (StatusCode::INTERNAL_SERVER_ERROR, Html("Server error".to_string())).into_response()
83 +
        }
84 +
    }
85 +
}
86 +
87 +
pub async fn api_list_wines(State(state): State<Arc<AppState>>) -> Response {
88 +
    match db::get_cellar_wines(&state.db) {
89 +
        Ok(wines) => Json(wines).into_response(),
90 +
        Err(e) => {
91 +
            tracing::error!("api_list_wines: {}", e);
92 +
            StatusCode::INTERNAL_SERVER_ERROR.into_response()
93 +
        }
94 +
    }
95 +
}
96 +
97 +
pub async fn api_get_wine(
98 +
    State(state): State<Arc<AppState>>,
99 +
    Path(short_id): Path<String>,
100 +
) -> Response {
101 +
    match db::get_wine_by_short_id(&state.db, &short_id) {
102 +
        Ok(Some(wine)) => Json(wine).into_response(),
103 +
        Ok(None) => StatusCode::NOT_FOUND.into_response(),
104 +
        Err(e) => {
105 +
            tracing::error!("api_get_wine: {}", e);
106 +
            StatusCode::INTERNAL_SERVER_ERROR.into_response()
82 107
        }
83 108
    }
84 109
}
apps/cellar/src/server/mod.rs +11 −0
7 7
use image::ImageDecoder;
8 8
use rust_embed::Embed;
9 9
use std::sync::Arc;
10 +
use tower_http::cors::{Any, CorsLayer};
10 11
11 12
use crate::db::{self, Db, Wine};
12 13
534 535
        site_description,
535 536
    });
536 537
538 +
    let cors = CorsLayer::new()
539 +
        .allow_origin(Any)
540 +
        .allow_methods([axum::http::Method::GET]);
541 +
542 +
    let api = Router::new()
543 +
        .route("/api/wines", get(public::api_list_wines))
544 +
        .route("/api/wines/{short_id}", get(public::api_get_wine))
545 +
        .layer(cors);
546 +
537 547
    let app = Router::new()
538 548
        // Public routes
539 549
        .route("/", get(public::get_index))
540 550
        .route("/feed.xml", get(public::rss_feed))
541 551
        .route("/wines/{short_id}", get(public::get_wine_detail))
542 552
        .route("/wines/{short_id}/image", get(public::get_wine_image))
553 +
        .merge(api)
543 554
        // Admin auth routes
544 555
        .route("/admin/login", get(admin::get_login).post(admin::post_login))
545 556
        .route("/admin/logout", get(admin::get_logout))