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}