1use axum::{
4 extract::{Path, State},
5 http::StatusCode,
6 Json,
7};
8use serde::{Deserialize, Serialize};
9use serde_with::skip_serializing_none;
10
11use cloudillo_core::extract::OptionalRequestId;
12use cloudillo_core::Auth;
13use cloudillo_types::{
14 auth_adapter::{ApiKeyInfo, CreateApiKeyOptions},
15 types::ApiResponse,
16};
17
18use crate::prelude::*;
19
20#[derive(Deserialize)]
22pub struct CreateApiKeyReq {
23 name: Option<String>,
24 scopes: Option<String>,
25 #[serde(rename = "expiresAt")]
26 expires_at: Option<i64>,
27}
28
29#[skip_serializing_none]
31#[derive(Serialize)]
32pub struct CreateApiKeyRes {
33 #[serde(rename = "keyId")]
34 key_id: i64,
35 #[serde(rename = "keyPrefix")]
36 key_prefix: String,
37 #[serde(rename = "plaintextKey")]
38 plaintext_key: String,
39 name: Option<String>,
40 scopes: Option<String>,
41 #[serde(rename = "expiresAt")]
42 expires_at: Option<i64>,
43 #[serde(rename = "createdAt")]
44 created_at: i64,
45}
46
47#[skip_serializing_none]
49#[derive(Serialize)]
50pub struct ApiKeyListItem {
51 #[serde(rename = "keyId")]
52 key_id: i64,
53 #[serde(rename = "keyPrefix")]
54 key_prefix: String,
55 name: Option<String>,
56 scopes: Option<String>,
57 #[serde(rename = "expiresAt")]
58 expires_at: Option<i64>,
59 #[serde(rename = "lastUsedAt")]
60 last_used_at: Option<i64>,
61 #[serde(rename = "createdAt")]
62 created_at: i64,
63}
64
65impl From<ApiKeyInfo> for ApiKeyListItem {
66 fn from(info: ApiKeyInfo) -> Self {
67 Self {
68 key_id: info.key_id,
69 key_prefix: info.key_prefix.to_string(),
70 name: info.name.map(|s| s.to_string()),
71 scopes: info.scopes.map(|s| s.to_string()),
72 expires_at: info.expires_at.map(|t| t.0),
73 last_used_at: info.last_used_at.map(|t| t.0),
74 created_at: info.created_at.0,
75 }
76 }
77}
78
79#[derive(Deserialize)]
81pub struct UpdateApiKeyReq {
82 name: Option<String>,
83 scopes: Option<String>,
84 #[serde(rename = "expiresAt")]
85 expires_at: Option<i64>,
86}
87
88pub async fn create_api_key(
90 State(app): State<App>,
91 Auth(auth): Auth,
92 OptionalRequestId(req_id): OptionalRequestId,
93 Json(req): Json<CreateApiKeyReq>,
94) -> ClResult<(StatusCode, Json<ApiResponse<CreateApiKeyRes>>)> {
95 info!("Creating API key for tenant {}", auth.id_tag);
96
97 let opts = CreateApiKeyOptions {
98 name: req.name.as_deref(),
99 scopes: req.scopes.as_deref(),
100 expires_at: req.expires_at.map(Timestamp),
101 };
102
103 let created = app.auth_adapter.create_api_key(auth.tn_id, opts).await?;
104
105 let response_data = CreateApiKeyRes {
106 key_id: created.info.key_id,
107 key_prefix: created.info.key_prefix.to_string(),
108 plaintext_key: created.plaintext_key.to_string(),
109 name: created.info.name.map(|s| s.to_string()),
110 scopes: created.info.scopes.map(|s| s.to_string()),
111 expires_at: created.info.expires_at.map(|t| t.0),
112 created_at: created.info.created_at.0,
113 };
114
115 info!(
116 "Created API key {} ({}) for tenant {}",
117 response_data.key_id, response_data.key_prefix, auth.id_tag
118 );
119
120 let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
121 Ok((StatusCode::CREATED, Json(response)))
122}
123
124pub async fn list_api_keys(
126 State(app): State<App>,
127 Auth(auth): Auth,
128 OptionalRequestId(req_id): OptionalRequestId,
129) -> ClResult<(StatusCode, Json<ApiResponse<Vec<ApiKeyListItem>>>)> {
130 let keys = app.auth_adapter.list_api_keys(auth.tn_id).await?;
131
132 let response_data: Vec<ApiKeyListItem> = keys.into_iter().map(Into::into).collect();
133
134 let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
135 Ok((StatusCode::OK, Json(response)))
136}
137
138pub async fn get_api_key(
140 State(app): State<App>,
141 Auth(auth): Auth,
142 Path(key_id): Path<i64>,
143 OptionalRequestId(req_id): OptionalRequestId,
144) -> ClResult<(StatusCode, Json<ApiResponse<ApiKeyListItem>>)> {
145 let key = app.auth_adapter.read_api_key(auth.tn_id, key_id).await?;
146
147 let response_data: ApiKeyListItem = key.into();
148
149 let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
150 Ok((StatusCode::OK, Json(response)))
151}
152
153pub async fn update_api_key(
155 State(app): State<App>,
156 Auth(auth): Auth,
157 Path(key_id): Path<i64>,
158 OptionalRequestId(req_id): OptionalRequestId,
159 Json(req): Json<UpdateApiKeyReq>,
160) -> ClResult<(StatusCode, Json<ApiResponse<ApiKeyListItem>>)> {
161 info!("Updating API key {} for tenant {}", key_id, auth.id_tag);
162
163 let updated = app
164 .auth_adapter
165 .update_api_key(
166 auth.tn_id,
167 key_id,
168 req.name.as_deref(),
169 req.scopes.as_deref(),
170 req.expires_at.map(Timestamp),
171 )
172 .await?;
173
174 let response_data: ApiKeyListItem = updated.into();
175
176 info!("Updated API key {} for tenant {}", key_id, auth.id_tag);
177
178 let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
179 Ok((StatusCode::OK, Json(response)))
180}
181
182pub async fn delete_api_key(
184 State(app): State<App>,
185 Auth(auth): Auth,
186 Path(key_id): Path<i64>,
187 OptionalRequestId(req_id): OptionalRequestId,
188) -> ClResult<(StatusCode, Json<ApiResponse<()>>)> {
189 info!("Deleting API key {} for tenant {}", key_id, auth.id_tag);
190
191 app.auth_adapter.delete_api_key(auth.tn_id, key_id).await?;
192
193 info!("Deleted API key {} for tenant {}", key_id, auth.id_tag);
194
195 let response = ApiResponse::new(()).with_req_id(req_id.unwrap_or_default());
196 Ok((StatusCode::OK, Json(response)))
197}
198
199