1use axum::{
4 extract::{Path, Query, State},
5 http::StatusCode,
6 Json,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::prelude::*;
11use cloudillo_core::extract::{OptionalAuth, OptionalRequestId};
12use cloudillo_types::meta_adapter::{CreateRefOptions, ListRefsOptions, RefData};
13use cloudillo_types::types::{serialize_timestamp_iso, serialize_timestamp_iso_opt, ApiResponse};
14use cloudillo_types::utils;
15
16#[serde_with::skip_serializing_none]
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct RefResponse {
20 #[serde(rename = "refId")]
21 pub ref_id: String,
22 pub r#type: String,
23 pub description: Option<String>,
24 #[serde(rename = "createdAt", serialize_with = "serialize_timestamp_iso")]
25 pub created_at: Timestamp,
26 #[serde(
27 rename = "expiresAt",
28 serialize_with = "serialize_timestamp_iso_opt",
29 skip_serializing_if = "Option::is_none"
30 )]
31 pub expires_at: Option<Timestamp>,
32 pub count: Option<u32>,
34 #[serde(rename = "resourceId")]
36 pub resource_id: Option<String>,
37 #[serde(rename = "accessLevel")]
39 pub access_level: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct RefResponseMinimal {
45 #[serde(rename = "refId")]
46 pub ref_id: String,
47 pub r#type: String,
48}
49
50impl From<RefData> for RefResponse {
51 fn from(ref_data: RefData) -> Self {
52 Self {
53 ref_id: ref_data.ref_id.to_string(),
54 r#type: ref_data.r#type.to_string(),
55 description: ref_data.description.map(|d| d.to_string()),
56 created_at: ref_data.created_at,
57 expires_at: ref_data.expires_at,
58 count: ref_data.count,
59 resource_id: ref_data.resource_id.map(|s| s.to_string()),
60 access_level: ref_data
61 .access_level
62 .map(|c| if c == 'W' { "write" } else { "read" }.to_string()),
63 }
64 }
65}
66
67impl From<RefData> for RefResponseMinimal {
68 fn from(ref_data: RefData) -> Self {
69 Self { ref_id: ref_data.ref_id.to_string(), r#type: ref_data.r#type.to_string() }
70 }
71}
72
73#[derive(Debug, Deserialize)]
75pub struct CreateRefRequest {
76 pub r#type: String,
78 pub description: Option<String>,
80 pub expires_at: Option<i64>,
82 #[serde(default)]
87 pub count: Patch<u32>,
88 #[serde(rename = "resourceId")]
90 pub resource_id: Option<String>,
91 #[serde(rename = "accessLevel")]
93 pub access_level: Option<String>,
94}
95
96#[derive(Debug, Deserialize, Default)]
98pub struct ListRefsQuery {
99 pub r#type: Option<String>,
101 pub filter: Option<String>,
103 #[serde(rename = "resourceId")]
105 pub resource_id: Option<String>,
106}
107
108pub use crate::service::{create_ref_internal, CreateRefInternalParams};
110
111#[axum::debug_handler]
113pub async fn list_refs(
114 State(app): State<App>,
115 tn_id: TnId,
116 Query(query_params): Query<ListRefsQuery>,
117 OptionalRequestId(req_id): OptionalRequestId,
118) -> ClResult<(StatusCode, Json<ApiResponse<Vec<RefResponse>>>)> {
119 info!(
120 tn_id = ?tn_id,
121 r#type = ?query_params.r#type,
122 filter = ?query_params.filter,
123 resource_id = ?query_params.resource_id,
124 "GET /api/refs - Listing refs"
125 );
126
127 let opts = ListRefsOptions {
128 typ: query_params.r#type,
129 filter: query_params.filter.or(Some("active".to_string())),
130 resource_id: query_params.resource_id,
131 };
132
133 let refs = app.meta_adapter.list_refs(tn_id, &opts).await?;
134
135 let response_data: Vec<RefResponse> = refs.into_iter().map(RefResponse::from).collect();
136
137 let total = response_data.len();
138 let mut response = ApiResponse::with_pagination(response_data, 0, total, total);
139 if let Some(id) = req_id {
140 response = response.with_req_id(id);
141 }
142
143 Ok((StatusCode::OK, Json(response)))
144}
145
146#[axum::debug_handler]
148pub async fn create_ref(
149 State(app): State<App>,
150 tn_id: TnId,
151 OptionalRequestId(req_id): OptionalRequestId,
152 Json(create_req): Json<CreateRefRequest>,
153) -> ClResult<(StatusCode, Json<ApiResponse<RefResponse>>)> {
154 info!(
155 tn_id = ?tn_id,
156 ref_type = %create_req.r#type,
157 description = ?create_req.description,
158 resource_id = ?create_req.resource_id,
159 access_level = ?create_req.access_level,
160 "POST /api/refs - Creating new ref"
161 );
162
163 if create_req.r#type.is_empty() {
165 return Err(Error::ValidationError("ref type is required".to_string()));
166 }
167
168 if let Some(expires_timestamp) = create_req.expires_at {
170 let expiration = Timestamp(expires_timestamp);
171 if expiration.0 <= Timestamp::now().0 {
172 return Err(Error::ValidationError(
173 "Expiration time must be in the future".to_string(),
174 ));
175 }
176 }
177
178 let access_level_char = match create_req.access_level.as_deref() {
180 Some("write" | "W") => Some('W'),
181 Some("read" | "R") | None => {
182 if create_req.resource_id.is_some() {
184 Some('R')
185 } else {
186 None
187 }
188 }
189 Some(other) => {
190 return Err(Error::ValidationError(format!(
191 "Invalid access_level '{}': must be 'read' or 'write'",
192 other
193 )));
194 }
195 };
196
197 if create_req.r#type == "share.file" && create_req.resource_id.is_none() {
199 return Err(Error::ValidationError(
200 "resource_id is required for share.file type".to_string(),
201 ));
202 }
203
204 let ref_id = utils::random_id()?;
205
206 let count = match create_req.count {
211 Patch::Undefined => Some(1),
212 Patch::Null => None,
213 Patch::Value(n) => Some(n),
214 };
215
216 let opts = CreateRefOptions {
217 typ: create_req.r#type.clone(),
218 description: create_req.description.clone(),
219 expires_at: create_req.expires_at.map(Timestamp),
220 count,
221 resource_id: create_req.resource_id.clone(),
222 access_level: access_level_char,
223 };
224
225 let ref_data = app.meta_adapter.create_ref(tn_id, &ref_id, &opts).await.map_err(|e| {
226 warn!("Failed to create ref: {}", e);
227 e
228 })?;
229
230 let response_data = RefResponse::from(ref_data);
231 let mut response = ApiResponse::new(response_data);
232 if let Some(id) = req_id {
233 response = response.with_req_id(id);
234 }
235
236 Ok((StatusCode::CREATED, Json(response)))
237}
238
239#[axum::debug_handler]
243pub async fn get_ref(
244 State(app): State<App>,
245 tn_id: TnId,
246 OptionalAuth(auth): OptionalAuth,
247 Path(ref_id): Path<String>,
248 OptionalRequestId(req_id): OptionalRequestId,
249) -> ClResult<(StatusCode, Json<serde_json::Value>)> {
250 let is_authenticated = auth.is_some();
251
252 info!(
253 tn_id = ?tn_id,
254 ref_id = %ref_id,
255 authenticated = is_authenticated,
256 "GET /api/refs/:id - Getting ref"
257 );
258
259 app.meta_adapter.get_ref(tn_id, &ref_id).await?.ok_or(Error::NotFound)?;
261
262 let opts = ListRefsOptions { typ: None, filter: Some("all".to_string()), resource_id: None };
266
267 let refs = app.meta_adapter.list_refs(tn_id, &opts).await?;
268 let ref_data = refs
269 .into_iter()
270 .find(|r| r.ref_id.as_ref() == ref_id.as_str())
271 .ok_or(Error::NotFound)?;
272
273 let response_value = if is_authenticated {
275 let response_data = RefResponse::from(ref_data);
277 let mut response = ApiResponse::new(response_data);
278 if let Some(id) = req_id {
279 response = response.with_req_id(id);
280 }
281 serde_json::to_value(response)?
282 } else {
283 let response_data = RefResponseMinimal::from(ref_data);
285 let mut response = ApiResponse::new(response_data);
286 if let Some(id) = req_id {
287 response = response.with_req_id(id);
288 }
289 serde_json::to_value(response)?
290 };
291
292 Ok((StatusCode::OK, Json(response_value)))
293}
294
295#[axum::debug_handler]
297pub async fn delete_ref(
298 State(app): State<App>,
299 tn_id: TnId,
300 Path(ref_id): Path<String>,
301 OptionalRequestId(req_id): OptionalRequestId,
302) -> ClResult<(StatusCode, Json<ApiResponse<()>>)> {
303 info!(
304 tn_id = ?tn_id,
305 ref_id = %ref_id,
306 "DELETE /api/refs/:id - Deleting ref"
307 );
308
309 app.meta_adapter.get_ref(tn_id, &ref_id).await?.ok_or(Error::NotFound)?;
311
312 app.meta_adapter.delete_ref(tn_id, &ref_id).await.map_err(|e| {
314 warn!("Failed to delete ref: {}", e);
315 e
316 })?;
317
318 let mut response = ApiResponse::new(());
319 if let Some(id) = req_id {
320 response = response.with_req_id(id);
321 }
322
323 Ok((StatusCode::OK, Json(response)))
324}
325
326