Skip to main content

cloudillo_auth/
api_key.rs

1//! API Key management endpoints
2
3use 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/// Request to create a new API key
21#[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/// Response for creating an API key (includes plaintext key shown only once)
30#[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/// Response for API key list/read operations
48#[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/// Request to update an API key
80#[derive(Deserialize)]
81pub struct UpdateApiKeyReq {
82	name: Option<String>,
83	scopes: Option<String>,
84	#[serde(rename = "expiresAt")]
85	expires_at: Option<i64>,
86}
87
88/// POST /api/auth/api-keys - Create a new API key
89pub 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
124/// GET /api/auth/api-keys - List all API keys for the authenticated tenant
125pub 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
138/// GET /api/auth/api-keys/{key_id} - Get a specific API key
139pub 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
153/// PATCH /api/auth/api-keys/{key_id} - Update an API key
154pub 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
182/// DELETE /api/auth/api-keys/{key_id} - Delete an API key
183pub 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// vim: ts=4