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