openauth_plugins/api_key/routes/
list.rs1use std::cmp::Ordering;
2use std::collections::BTreeSet;
3
4use http::{Method, StatusCode};
5use openauth_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) -> openauth_core::api::AsyncAuthEndpoint {
40 endpoint(
41 "/api-key/list",
42 Method::GET,
43 configurations,
44 |context, request, configurations| {
45 Box::pin(async move {
46 let config_id = query_param(&request, "configId");
47 let organization_id = query_param(&request, "organizationId");
48 let Some(identity) = current_identity(context, &request).await? else {
49 return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION);
50 };
51 let expected_reference = if organization_id.is_some() {
52 ApiKeyReference::Organization
53 } else {
54 ApiKeyReference::User
55 };
56 let reference_id = if let Some(organization_id) = organization_id {
57 if let Err(error) = ensure_organization_permission(
58 context,
59 &identity.user.id,
60 &organization_id,
61 ApiKeyAction::Read,
62 )
63 .await
64 {
65 return error_response_from_openauth(error);
66 }
67 organization_id
68 } else {
69 identity.user.id
70 };
71 let limit = query_usize(&request, "limit");
72 let offset = query_usize(&request, "offset");
73 let sort_direction = match query_param(&request, "sortDirection").as_deref() {
74 Some("desc") => SortDirection::Desc,
75 _ => SortDirection::Asc,
76 };
77 let sort_by = query_param(&request, "sortBy");
78 let mut api_keys = if let Some(config_id) = config_id.as_deref() {
79 let options = configurations.resolve(Some(config_id))?;
80 if options.reference != expected_reference {
81 Vec::new()
82 } else {
83 ApiKeyStore::new(context, &options)
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 }
102 } else {
103 list_all_configurations(
104 context,
105 &configurations,
106 &reference_id,
107 expected_reference,
108 sort_by.clone(),
109 sort_direction,
110 )
111 .await?
112 };
113 sort_api_keys(&mut api_keys, sort_by.as_deref(), sort_direction);
114 let total = api_keys.len() as u64;
115 let api_keys = paginate(api_keys, offset, limit)
116 .into_iter()
117 .map(|api_key| api_key.public())
118 .collect::<Vec<_>>();
119 json(
120 StatusCode::OK,
121 &ListApiKeysResponse {
122 api_keys,
123 total,
124 limit,
125 offset,
126 },
127 )
128 })
129 },
130 )
131}
132
133async fn list_all_configurations(
134 context: &openauth_core::context::AuthContext,
135 configurations: &SharedConfigurations,
136 reference_id: &str,
137 expected_reference: ApiKeyReference,
138 sort_by: Option<String>,
139 sort_direction: SortDirection,
140) -> Result<Vec<ApiKeyRecord>, openauth_core::error::OpenAuthError> {
141 let mut seen = BTreeSet::new();
142 let mut all = Vec::new();
143 for configuration in configurations.all() {
144 let options = configurations.resolve(configuration.config_id.as_deref())?;
145 if options.reference != expected_reference {
146 continue;
147 }
148 let config_id = options
149 .config_id
150 .clone()
151 .unwrap_or_else(|| "default".to_owned());
152 let result = ApiKeyStore::new(context, &options)
153 .list(
154 reference_id,
155 ListOptions {
156 config_id: Some(config_id),
157 limit: None,
158 offset: None,
159 sort_by: sort_by.clone(),
160 sort_direction,
161 },
162 )
163 .await?;
164 for api_key in result.api_keys {
165 if seen.insert(api_key.id.clone()) {
166 all.push(api_key);
167 }
168 }
169 }
170 Ok(all)
171}
172
173fn sort_api_keys(
174 api_keys: &mut [ApiKeyRecord],
175 sort_by: Option<&str>,
176 sort_direction: SortDirection,
177) {
178 let Some(sort_by) = sort_by else {
179 return;
180 };
181 api_keys.sort_by(|left, right| compare_api_keys(left, right, sort_by));
182 if sort_direction == SortDirection::Desc {
183 api_keys.reverse();
184 }
185}
186
187fn paginate(
188 api_keys: Vec<ApiKeyRecord>,
189 offset: Option<usize>,
190 limit: Option<usize>,
191) -> Vec<ApiKeyRecord> {
192 let iter = api_keys.into_iter().skip(offset.unwrap_or(0));
193 match limit {
194 Some(limit) => iter.take(limit).collect(),
195 None => iter.collect(),
196 }
197}
198
199fn compare_api_keys(left: &ApiKeyRecord, right: &ApiKeyRecord, field: &str) -> Ordering {
200 match field {
201 "createdAt" | "created_at" => left.created_at.cmp(&right.created_at),
202 "updatedAt" | "updated_at" => left.updated_at.cmp(&right.updated_at),
203 "name" => left.name.cmp(&right.name),
204 "expiresAt" | "expires_at" => left.expires_at.cmp(&right.expires_at),
205 _ => left.id.cmp(&right.id),
206 }
207}
208
209fn error_response_from_openauth(
210 error: openauth_core::error::OpenAuthError,
211) -> Result<openauth_core::api::ApiResponse, openauth_core::error::OpenAuthError> {
212 let message = error.to_string();
213 if message.contains(errors::message(errors::USER_NOT_MEMBER_OF_ORGANIZATION)) {
214 return super::error(
215 StatusCode::FORBIDDEN,
216 errors::USER_NOT_MEMBER_OF_ORGANIZATION,
217 );
218 }
219 if message.contains(errors::message(errors::ORGANIZATION_PLUGIN_REQUIRED)) {
220 return super::error(
221 StatusCode::INTERNAL_SERVER_ERROR,
222 errors::ORGANIZATION_PLUGIN_REQUIRED,
223 );
224 }
225 super::error(
226 StatusCode::FORBIDDEN,
227 errors::INSUFFICIENT_API_KEY_PERMISSIONS,
228 )
229}