openzeppelin_relayer/api/controllers/
api_key.rs

1//! # Api Key Controller
2//!
3//! Handles HTTP endpoints for api key operations including:
4//! - Create api keys
5//! - List api keys
6//! - Delete api keys
7use crate::{
8    jobs::JobProducerTrait,
9    models::{
10        ApiError, ApiKeyRepoModel, ApiKeyRequest, ApiKeyResponse, ApiResponse, NetworkRepoModel,
11        NotificationRepoModel, PaginationMeta, PaginationQuery, RelayerRepoModel, SignerRepoModel,
12        ThinDataAppState, TransactionRepoModel,
13    },
14    repositories::{
15        ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
16        Repository, TransactionCounterTrait, TransactionRepository,
17    },
18};
19use actix_web::HttpResponse;
20use eyre::Result;
21
22/// Create api key
23///
24/// # Arguments
25///
26/// * `api_key_request` - The api key request.
27///     * `name` - The name of the api key.
28///     * `allowed_origins` - The allowed origins for the api key.
29///     * `permissions` - The permissions for the api key.
30/// * `state` - The application state containing the api key repository.
31///
32/// # Returns
33///
34/// The result of the plugin call.
35pub async fn create_api_key<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
36    api_key_request: ApiKeyRequest,
37    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
38) -> Result<HttpResponse, ApiError>
39where
40    J: JobProducerTrait + Send + Sync + 'static,
41    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
42    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
43    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
44    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
45    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
46    TCR: TransactionCounterTrait + Send + Sync + 'static,
47    PR: PluginRepositoryTrait + Send + Sync + 'static,
48    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
49{
50    let api_key = ApiKeyRepoModel::try_from(api_key_request)?;
51
52    let api_key = state.api_key_repository.create(api_key).await?;
53
54    Ok(HttpResponse::Created().json(ApiResponse::success(api_key)))
55}
56
57/// List api keys
58///
59/// # Arguments
60///
61/// * `query` - The pagination query parameters.
62///     * `page` - The page number.
63///     * `per_page` - The number of items per page.
64/// * `state` - The application state containing the api key repository.
65///
66/// # Returns
67///
68/// The result of the api key list.
69pub async fn list_api_keys<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
70    query: PaginationQuery,
71    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
72) -> Result<HttpResponse, ApiError>
73where
74    J: JobProducerTrait + Send + Sync + 'static,
75    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
76    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
77    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
78    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
79    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
80    TCR: TransactionCounterTrait + Send + Sync + 'static,
81    PR: PluginRepositoryTrait + Send + Sync + 'static,
82    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
83{
84    let api_keys = state.api_key_repository.list_paginated(query).await?;
85
86    let api_key_items: Vec<ApiKeyRepoModel> = api_keys.items.into_iter().collect();
87
88    // Subtract the "value" from the api key to avoid exposing it.
89    let api_key_items: Vec<ApiKeyResponse> = api_key_items
90        .into_iter()
91        .map(ApiKeyResponse::try_from)
92        .collect::<Result<Vec<ApiKeyResponse>, ApiError>>()?;
93
94    Ok(HttpResponse::Ok().json(ApiResponse::paginated(
95        api_key_items,
96        PaginationMeta {
97            total_items: api_keys.total,
98            current_page: api_keys.page,
99            per_page: api_keys.per_page,
100        },
101    )))
102}
103
104/// Get api key permissions
105///
106/// # Arguments
107///
108/// * `api_key_id` - The id of the api key.
109/// * `state` - The application state containing the api key repository.
110///
111pub async fn get_api_key_permissions<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
112    api_key_id: String,
113    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
114) -> Result<HttpResponse, ApiError>
115where
116    J: JobProducerTrait + Send + Sync + 'static,
117    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
118    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
119    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
120    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
121    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
122    TCR: TransactionCounterTrait + Send + Sync + 'static,
123    PR: PluginRepositoryTrait + Send + Sync + 'static,
124    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
125{
126    let permissions = state
127        .api_key_repository
128        .list_permissions(&api_key_id)
129        .await?;
130
131    Ok(HttpResponse::Ok().json(ApiResponse::success(permissions)))
132}
133
134/// Delete api key
135///
136/// # Arguments
137///
138/// * `api_key_id` - The id of the api key.
139/// * `state` - The application state containing the api key repository.
140///
141pub async fn delete_api_key<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
142    _api_key_id: String,
143    _state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
144) -> Result<HttpResponse, ApiError>
145where
146    J: JobProducerTrait + Send + Sync + 'static,
147    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
148    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
149    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
150    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
151    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
152    TCR: TransactionCounterTrait + Send + Sync + 'static,
153    PR: PluginRepositoryTrait + Send + Sync + 'static,
154    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
155{
156    // state.api_key_repository.delete_by_id(&api_key_id).await?;
157
158    // Ok(HttpResponse::Ok().json(ApiResponse::success(api_key_id)))
159    Ok(HttpResponse::Ok().json(ApiResponse::<String>::error("Not implemented".to_string())))
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::{
166        models::{ApiKeyRepoModel, PaginationQuery, SecretString},
167        utils::mocks::mockutils::create_mock_app_state,
168    };
169    use actix_web::web::ThinData;
170
171    /// Helper function to create a test api key model
172    fn create_test_api_key_model(id: &str) -> ApiKeyRepoModel {
173        ApiKeyRepoModel {
174            id: id.to_string(),
175            value: SecretString::new("test-api-key-value"),
176            name: "Test API Key".to_string(),
177            allowed_origins: vec!["*".to_string()],
178            permissions: vec!["relayer:all:execute".to_string()],
179            created_at: "2023-01-01T00:00:00Z".to_string(),
180        }
181    }
182
183    /// Helper function to create a test api key create request
184    fn create_test_api_key_create_request(name: &str) -> ApiKeyRequest {
185        ApiKeyRequest {
186            name: name.to_string(),
187            permissions: vec!["relayer:all:execute".to_string()],
188            allowed_origins: Some(vec!["*".to_string()]),
189        }
190    }
191
192    #[actix_web::test]
193    async fn test_create_api_key() {
194        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
195        let api_key_request = create_test_api_key_create_request("Test API Key");
196
197        let result = create_api_key(api_key_request, ThinData(app_state)).await;
198
199        assert!(result.is_ok());
200        let response = result.unwrap();
201        assert_eq!(response.status(), 201);
202    }
203
204    #[actix_web::test]
205    async fn test_list_api_keys_empty() {
206        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
207        let query = PaginationQuery {
208            page: 1,
209            per_page: 10,
210        };
211
212        let result = list_api_keys(query, ThinData(app_state)).await;
213
214        assert!(result.is_ok());
215        let response = result.unwrap();
216        assert_eq!(response.status(), 200);
217    }
218
219    #[actix_web::test]
220    async fn test_list_api_keys_with_data() {
221        let api_key = create_test_api_key_model("test-api-key-1");
222        let app_state =
223            create_mock_app_state(Some(vec![api_key]), None, None, None, None, None).await;
224        let query = PaginationQuery {
225            page: 1,
226            per_page: 10,
227        };
228
229        let result = list_api_keys(query, ThinData(app_state)).await;
230
231        assert!(result.is_ok());
232        let response = result.unwrap();
233        assert_eq!(response.status(), 200);
234    }
235
236    #[actix_web::test]
237    async fn test_get_api_key_permissions() {
238        let api_key = create_test_api_key_model("test-api-key-1");
239        let api_key_id = api_key.id.clone();
240        let app_state =
241            create_mock_app_state(Some(vec![api_key]), None, None, None, None, None).await;
242
243        let result = get_api_key_permissions(api_key_id, ThinData(app_state)).await;
244
245        assert!(result.is_ok());
246        let response = result.unwrap();
247        assert_eq!(response.status(), 200);
248    }
249
250    // #[actix_web::test]
251    // async fn test_delete_api_key() {
252    //     let api_key = create_test_api_key_model("test-api-key-1");
253    //     let api_key_id = api_key.id.clone();
254    //     let app_state =
255    //         create_mock_app_state(Some(vec![api_key]), None, None, None, None, None).await;
256
257    //     let result = delete_api_key(api_key_id, ThinData(app_state)).await;
258
259    //     assert!(result.is_ok());
260    //     let response = result.unwrap();
261    //     assert_eq!(response.status(), 200);
262    // }
263
264    #[actix_web::test]
265    async fn test_get_permissions_nonexistent_api_key() {
266        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
267
268        let result =
269            get_api_key_permissions("nonexistent-id".to_string(), ThinData(app_state)).await;
270
271        assert!(result.is_err());
272    }
273}