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