Skip to main content

cloudillo_auth/
api_key.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! API Key management endpoints
5
6use 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/// Request to create a new API key
24#[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/// Response for creating an API key (includes plaintext key shown only once)
33#[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/// Response for API key list/read operations
51#[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/// Request to update an API key
83#[derive(Deserialize)]
84pub struct UpdateApiKeyReq {
85	name: Option<String>,
86	scopes: Option<String>,
87	#[serde(rename = "expiresAt")]
88	expires_at: Option<i64>,
89}
90
91/// POST /api/auth/api-keys - Create a new API key
92pub 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
127/// GET /api/auth/api-keys - List all API keys for the authenticated tenant
128pub 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
141/// GET /api/auth/api-keys/{key_id} - Get a specific API key
142pub 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
156/// PATCH /api/auth/api-keys/{key_id} - Update an API key
157pub 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
185/// DELETE /api/auth/api-keys/{key_id} - Delete an API key
186pub 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// vim: ts=4