Skip to main content

openauth_plugins/api_key/routes/
list.rs

1use 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}