rustauth_plugins/api_key/routes/
list.rs1use std::cmp::Ordering;
2use std::collections::BTreeSet;
3
4use http::{Method, StatusCode};
5use rustauth_core::db::SortDirection;
6use serde::{Deserialize, Serialize};
7
8use super::{
9 current_identity, endpoint, error, json, query_param, query_usize, SharedConfigurations,
10};
11use crate::api_key::errors;
12use crate::api_key::models::{ApiKeyPublicRecord, ApiKeyRecord};
13use crate::api_key::options::ApiKeyReference;
14use crate::api_key::organization::{ensure_organization_permission, ApiKeyAction};
15use crate::api_key::storage::{ApiKeyStore, ListOptions};
16
17#[derive(Debug, Clone, Deserialize, Serialize, Default)]
18#[serde(rename_all = "camelCase")]
19pub struct ListApiKeysQuery {
20 pub config_id: Option<String>,
21 pub organization_id: Option<String>,
22 pub limit: Option<usize>,
23 pub offset: Option<usize>,
24 pub sort_by: Option<String>,
25 pub sort_direction: Option<String>,
26}
27
28#[derive(Debug, Clone, Serialize)]
29#[serde(rename_all = "camelCase")]
30struct ListApiKeysResponse {
31 api_keys: Vec<ApiKeyPublicRecord>,
32 total: u64,
33 limit: Option<usize>,
34 offset: Option<usize>,
35}
36
37pub fn list_endpoint(
38 configurations: SharedConfigurations,
39) -> rustauth_core::api::AsyncAuthEndpoint {
40 endpoint(
41 "/api-key/list",
42 Method::GET,
43 configurations,
44 |context, request, configurations| async move {
45 let config_id = query_param(&request, "configId");
46 let organization_id = query_param(&request, "organizationId");
47 let Some(identity) = current_identity(&context, &request).await? else {
48 return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION);
49 };
50 let expected_reference = if organization_id.is_some() {
51 ApiKeyReference::Organization
52 } else {
53 ApiKeyReference::User
54 };
55 let reference_id = if let Some(organization_id) = organization_id {
56 if let Err(error) = ensure_organization_permission(
57 &context,
58 &identity.user.id,
59 &organization_id,
60 ApiKeyAction::Read,
61 )
62 .await
63 {
64 return error_response_from_rustauth(error);
65 }
66 organization_id
67 } else {
68 identity.user.id
69 };
70 let limit = query_usize(&request, "limit");
71 let offset = query_usize(&request, "offset");
72 let sort_direction = match query_param(&request, "sortDirection").as_deref() {
73 Some("desc") => SortDirection::Desc,
74 _ => SortDirection::Asc,
75 };
76 let sort_by = query_param(&request, "sortBy");
77 let mut api_keys = if let Some(config_id) = config_id.as_deref() {
78 let options = configurations.resolve(Some(config_id))?;
79 if options.reference != expected_reference {
80 Vec::new()
81 } else {
82 let store = ApiKeyStore::new(&context, &options);
83 let mut result = store
84 .list(
85 &reference_id,
86 ListOptions {
87 config_id: Some(
88 options
89 .config_id
90 .clone()
91 .unwrap_or_else(|| "default".to_owned()),
92 ),
93 limit: None,
94 offset: None,
95 sort_by: sort_by.clone(),
96 sort_direction,
97 },
98 )
99 .await?
100 .api_keys;
101 for api_key in &mut result {
102 store.migrate_metadata_if_needed(api_key).await;
103 }
104 result
105 }
106 } else {
107 list_all_configurations(
108 &context,
109 &configurations,
110 &reference_id,
111 expected_reference,
112 sort_by.clone(),
113 sort_direction,
114 )
115 .await?
116 };
117 sort_api_keys(&mut api_keys, sort_by.as_deref(), sort_direction);
118 let total = api_keys.len() as u64;
119 let api_keys = paginate(api_keys, offset, limit)
120 .into_iter()
121 .map(|api_key| api_key.public())
122 .collect::<Vec<_>>();
123 json(
124 StatusCode::OK,
125 &ListApiKeysResponse {
126 api_keys,
127 total,
128 limit,
129 offset,
130 },
131 )
132 },
133 )
134}
135
136async fn list_all_configurations(
137 context: &rustauth_core::context::AuthContext,
138 configurations: &SharedConfigurations,
139 reference_id: &str,
140 expected_reference: ApiKeyReference,
141 sort_by: Option<String>,
142 sort_direction: SortDirection,
143) -> Result<Vec<ApiKeyRecord>, rustauth_core::error::RustAuthError> {
144 let mut seen = BTreeSet::new();
145 let mut all = Vec::new();
146 for configuration in configurations.all() {
147 let options = configurations.resolve(configuration.config_id.as_deref())?;
148 if options.reference != expected_reference {
149 continue;
150 }
151 let config_id = options
152 .config_id
153 .clone()
154 .unwrap_or_else(|| "default".to_owned());
155 let store = ApiKeyStore::new(context, &options);
156 let mut result = store
157 .list(
158 reference_id,
159 ListOptions {
160 config_id: Some(config_id),
161 limit: None,
162 offset: None,
163 sort_by: sort_by.clone(),
164 sort_direction,
165 },
166 )
167 .await?;
168 for api_key in &mut result.api_keys {
169 store.migrate_metadata_if_needed(api_key).await;
170 }
171 for api_key in result.api_keys {
172 if seen.insert(api_key.id.clone()) {
173 all.push(api_key);
174 }
175 }
176 }
177 Ok(all)
178}
179
180fn sort_api_keys(
181 api_keys: &mut [ApiKeyRecord],
182 sort_by: Option<&str>,
183 sort_direction: SortDirection,
184) {
185 let Some(sort_by) = sort_by else {
186 return;
187 };
188 api_keys.sort_by(|left, right| compare_api_keys(left, right, sort_by));
189 if sort_direction == SortDirection::Desc {
190 api_keys.reverse();
191 }
192}
193
194fn paginate(
195 api_keys: Vec<ApiKeyRecord>,
196 offset: Option<usize>,
197 limit: Option<usize>,
198) -> Vec<ApiKeyRecord> {
199 let iter = api_keys.into_iter().skip(offset.unwrap_or(0));
200 match limit {
201 Some(limit) => iter.take(limit).collect(),
202 None => iter.collect(),
203 }
204}
205
206fn compare_api_keys(left: &ApiKeyRecord, right: &ApiKeyRecord, field: &str) -> Ordering {
207 match field {
208 "createdAt" | "created_at" => left.created_at.cmp(&right.created_at),
209 "updatedAt" | "updated_at" => left.updated_at.cmp(&right.updated_at),
210 "name" => left.name.cmp(&right.name),
211 "expiresAt" | "expires_at" => left.expires_at.cmp(&right.expires_at),
212 _ => left.id.cmp(&right.id),
213 }
214}
215
216fn error_response_from_rustauth(
217 error: rustauth_core::error::RustAuthError,
218) -> Result<rustauth_core::api::ApiResponse, rustauth_core::error::RustAuthError> {
219 let message = error.to_string();
220 if message.contains(errors::message(errors::USER_NOT_MEMBER_OF_ORGANIZATION)) {
221 return super::error(
222 StatusCode::FORBIDDEN,
223 errors::USER_NOT_MEMBER_OF_ORGANIZATION,
224 );
225 }
226 if message.contains(errors::message(errors::ORGANIZATION_PLUGIN_REQUIRED)) {
227 return super::error(
228 StatusCode::INTERNAL_SERVER_ERROR,
229 errors::ORGANIZATION_PLUGIN_REQUIRED,
230 );
231 }
232 super::error(
233 StatusCode::FORBIDDEN,
234 errors::INSUFFICIENT_API_KEY_PERMISSIONS,
235 )
236}