Skip to main content

fraiseql_server/routes/studio/
storage_browser.rs

1//! Storage browser endpoints for the Studio dashboard.
2//!
3//! Routes under `/admin/v1/storage/*` expose bucket listing, object listing
4//! with prefix filtering, presigned URL generation, and object deletion.
5//! All routes are protected by the admin bearer token middleware.
6
7use axum::{
8    Json,
9    extract::{Query, State},
10    http::StatusCode,
11    response::IntoResponse,
12};
13use fraiseql_core::db::traits::DatabaseAdapter;
14use serde::{Deserialize, Serialize};
15
16use crate::routes::graphql::app_state::AppState;
17
18// ---------------------------------------------------------------------------
19// Object record
20// ---------------------------------------------------------------------------
21
22/// A single object entry in the storage browser.
23///
24/// Agreed response shape with the Luxen UI author.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ObjectEntry {
27    /// Object key (path within the bucket).
28    pub key:          String,
29    /// Object size in bytes.
30    pub size:         u64,
31    /// MIME content type.
32    pub content_type: String,
33    /// Last-modified timestamp (RFC 3339).
34    pub updated_at:   String,
35}
36
37/// A storage bucket summary.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BucketEntry {
40    /// Bucket name.
41    pub name:         String,
42    /// Number of objects in the bucket.
43    pub object_count: u64,
44}
45
46// ---------------------------------------------------------------------------
47// Response types
48// ---------------------------------------------------------------------------
49
50/// Paginated object list response agreed with the Luxen UI author.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ObjectListResponse {
53    /// Objects on this page.
54    pub objects:   Vec<ObjectEntry>,
55    /// Total object count.
56    pub total:     u64,
57    /// Current page number (1-indexed).
58    pub page:      u32,
59    /// Objects per page.
60    pub page_size: u32,
61}
62
63/// Bucket list response.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct BucketListResponse {
66    /// All buckets for this tenant.
67    pub buckets: Vec<BucketEntry>,
68}
69
70// ---------------------------------------------------------------------------
71// Request types
72// ---------------------------------------------------------------------------
73
74/// Query parameters for `GET /admin/v1/storage/objects`.
75#[derive(Debug, Clone, Deserialize)]
76pub struct ObjectListQuery {
77    /// Bucket to list.
78    pub bucket:    String,
79    /// Key prefix filter (optional).
80    pub prefix:    Option<String>,
81    /// Page number (1-indexed, default 1).
82    #[serde(default = "default_page")]
83    pub page:      u32,
84    /// Objects per page (default 50).
85    #[serde(default = "default_page_size")]
86    pub page_size: u32,
87}
88
89const fn default_page() -> u32 {
90    1
91}
92
93const fn default_page_size() -> u32 {
94    50
95}
96
97/// Request body for `POST /admin/v1/storage/objects/sign`.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct PresignRequest {
100    /// Bucket containing the object.
101    pub bucket:          String,
102    /// Object key.
103    pub key:             String,
104    /// URL expiry in seconds.
105    pub expires_in_secs: u32,
106}
107
108/// Request body for `DELETE /admin/v1/storage/objects`.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DeleteObjectRequest {
111    /// Bucket containing the object.
112    pub bucket: String,
113    /// Object key.
114    pub key:    String,
115}
116
117// ---------------------------------------------------------------------------
118// Handlers
119// ---------------------------------------------------------------------------
120
121/// `GET /admin/v1/storage/buckets` — list all buckets for the tenant.
122///
123/// # Errors
124///
125/// Returns `401` without valid admin credentials (enforced by middleware).
126pub async fn list_buckets_handler<A>(State(_state): State<AppState<A>>) -> impl IntoResponse
127where
128    A: DatabaseAdapter + Clone + Send + Sync + 'static,
129{
130    // Placeholder — not yet wired to StorageBackend.
131    Json(BucketListResponse { buckets: vec![] })
132}
133
134/// `GET /admin/v1/storage/objects` — paginated object list with prefix filtering.
135///
136/// # Errors
137///
138/// Returns `401` without valid admin credentials (enforced by middleware).
139/// Returns `404` if the bucket does not exist.
140pub async fn list_objects_handler<A>(
141    State(_state): State<AppState<A>>,
142    Query(_params): Query<ObjectListQuery>,
143) -> impl IntoResponse
144where
145    A: DatabaseAdapter + Clone + Send + Sync + 'static,
146{
147    Json(ObjectListResponse {
148        objects:   vec![],
149        total:     0,
150        page:      1,
151        page_size: 50,
152    })
153}
154
155/// `POST /admin/v1/storage/objects/sign` — generate a presigned URL.
156///
157/// # Errors
158///
159/// Returns `401` without valid admin credentials (enforced by middleware).
160pub async fn presign_handler<A>(
161    State(_state): State<AppState<A>>,
162    Json(req): Json<PresignRequest>,
163) -> impl IntoResponse
164where
165    A: DatabaseAdapter + Clone + Send + Sync + 'static,
166{
167    (
168        StatusCode::NOT_IMPLEMENTED,
169        Json(serde_json::json!({
170            "error": "Not Implemented",
171            "message": format!(
172                "Presign for {}/{} not yet wired",
173                req.bucket, req.key
174            )
175        })),
176    )
177}
178
179/// `DELETE /admin/v1/storage/objects` — delete an object by bucket + key.
180///
181/// # Errors
182///
183/// Returns `401` without valid admin credentials (enforced by middleware).
184pub async fn delete_object_handler<A>(
185    State(_state): State<AppState<A>>,
186    Json(req): Json<DeleteObjectRequest>,
187) -> impl IntoResponse
188where
189    A: DatabaseAdapter + Clone + Send + Sync + 'static,
190{
191    (
192        StatusCode::NOT_IMPLEMENTED,
193        Json(serde_json::json!({
194            "error": "Not Implemented",
195            "message": format!(
196                "Delete {}/{} not yet wired",
197                req.bucket, req.key
198            )
199        })),
200    )
201}