Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1e12db0
feat: Add support to plugin list endpoint
MCarlomagno Jul 10, 2025
7b09841
docs: Add plugin list to docs
MCarlomagno Jul 10, 2025
2aa09db
fix: Use paginated list
MCarlomagno Jul 11, 2025
58a497d
fix: Merge main into branch
MCarlomagno Jul 11, 2025
f3b0b9b
fix: Lint
MCarlomagno Jul 11, 2025
6b7fb42
feat: Create API keys CRUD methods
MCarlomagno Jul 24, 2025
4476acb
fix: Merge main to branch
MCarlomagno Jul 24, 2025
58c3f4c
fix: Examoles linting
MCarlomagno Jul 24, 2025
86b53d8
chore: Hide API key feature under feature flag
MCarlomagno Jul 24, 2025
85072c1
fix: Rename flag to authV2
MCarlomagno Jul 25, 2025
9761344
test: Add redis tests + docs
MCarlomagno Jul 25, 2025
68462d4
test: Add repository tests
MCarlomagno Jul 25, 2025
3d00037
test: Add remaining tests
MCarlomagno Jul 25, 2025
745435e
fix: Merge conflicts with main
MCarlomagno Aug 25, 2025
fc3205c
chore: Update changelog
MCarlomagno Aug 25, 2025
0c10e39
fix: Merge main
MCarlomagno Aug 27, 2025
c7e2faa
test: Fix tests
MCarlomagno Aug 27, 2025
77c16f9
fix: Rollback wiremock change
MCarlomagno Aug 28, 2025
5859a4c
fix: Merge main into branch
MCarlomagno Aug 28, 2025
547d524
refactor: Split models into files
MCarlomagno Aug 28, 2025
8747994
fix: Remove feature flag + use SecureString
MCarlomagno Aug 28, 2025
d616cad
feat: Serialize and deserialize secret streing
MCarlomagno Aug 28, 2025
b8b6567
refactor: Use tryfrom for request handling
MCarlomagno Aug 28, 2025
b2bf823
fix: Coderabbit fixes
MCarlomagno Sep 1, 2025
d721c15
feat: Load default key in repo
MCarlomagno Sep 3, 2025
16ed277
chore: Merge main into branch
MCarlomagno Sep 4, 2025
ad18999
fix: Merge conflicts
MCarlomagno Sep 22, 2025
9291ebf
test: Add routes tests
MCarlomagno Sep 22, 2025
3528ded
fix: Merge conflicts
MCarlomagno Sep 23, 2025
87f5f5e
fix: Avoid deleting API keys
MCarlomagno Sep 23, 2025
5e56510
fix: Merge conflicts with main
MCarlomagno Sep 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,12 +278,32 @@ cargo test properties
cargo test integration
```


> :warning: Debian/Ubuntu: If you encounter OpenSSL build errors, install the required packages:

```bash
sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev
```

#### Run tests against Redis

Comment thread
zeljkoX marked this conversation as resolved.
1. You can start a Redis instance using the following command:

```bash
docker run -d \
--name redis \
-p 6379:6379 \
redis:latest
```

2. Then remove the `#[ignore = "Requires active Redis instance"]` attribute from the tests you want to run.

3. Run the tests using single thread to avoid race conditions within suites:

```bash
cargo test your_test_regex -- --test-threads=1


### Config files

Create `config/config.json` file. You can use `config/config.example.json` as a starting point:
Expand Down
273 changes: 273 additions & 0 deletions src/api/controllers/api_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
//! # Api Key Controller
//!
//! Handles HTTP endpoints for api key operations including:
//! - Create api keys
//! - List api keys
//! - Delete api keys
use crate::{
jobs::JobProducerTrait,
models::{
ApiError, ApiKeyRepoModel, ApiKeyRequest, ApiKeyResponse, ApiResponse, NetworkRepoModel,
NotificationRepoModel, PaginationMeta, PaginationQuery, RelayerRepoModel, SignerRepoModel,
ThinDataAppState, TransactionRepoModel,
},
repositories::{
ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
Repository, TransactionCounterTrait, TransactionRepository,
},
};
use actix_web::HttpResponse;
use eyre::Result;

Comment on lines +19 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

eyre::Result import breaks compilation; wrong generic arity in collect.

use eyre::Result; shadows std::result::Result. Then collect::<Result<_, ApiError>>()? uses the eyre alias, which only takes 1 type parameter. This won’t compile.

Apply this diff:

-use eyre::Result;
@@
-        .collect::<Result<Vec<ApiKeyResponse>, ApiError>>()?;
+        .collect::<std::result::Result<Vec<ApiKeyResponse>, ApiError>>()?;

Alternatively, just remove the eyre import; the prelude Result will work with two generics.

Also applies to: 89-93

🤖 Prompt for AI Agents
In src/api/controllers/api_key.rs around lines 19-21 (and also check lines
89-93), remove the `use eyre::Result;` import because it shadows the standard
Result and breaks usages like `collect::<Result<_, ApiError>>()`; instead rely
on the std Result (or explicitly use `std::result::Result`) so the two-generic
`Result<T, E>` resolves correctly; update any affected collect/type annotations
if needed to use `Result<_, ApiError>` from std or fully qualify as
`std::result::Result<_, ApiError>`.

/// Create api key
///
/// # Arguments
///
/// * `api_key_request` - The api key request.
/// * `name` - The name of the api key.
/// * `allowed_origins` - The allowed origins for the api key.
/// * `permissions` - The permissions for the api key.
/// * `state` - The application state containing the api key repository.
///
/// # Returns
///
/// The result of the plugin call.
pub async fn create_api_key<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
api_key_request: ApiKeyRequest,
state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
) -> Result<HttpResponse, ApiError>
where
J: JobProducerTrait + Send + Sync + 'static,
RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
TCR: TransactionCounterTrait + Send + Sync + 'static,
PR: PluginRepositoryTrait + Send + Sync + 'static,
AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
{
let api_key = ApiKeyRepoModel::try_from(api_key_request)?;

let api_key = state.api_key_repository.create(api_key).await?;

Ok(HttpResponse::Created().json(ApiResponse::success(api_key)))
}
Comment thread
MCarlomagno marked this conversation as resolved.

/// List api keys
///
/// # Arguments
///
/// * `query` - The pagination query parameters.
/// * `page` - The page number.
/// * `per_page` - The number of items per page.
/// * `state` - The application state containing the api key repository.
///
/// # Returns
///
/// The result of the api key list.
pub async fn list_api_keys<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
query: PaginationQuery,
state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
) -> Result<HttpResponse, ApiError>
where
J: JobProducerTrait + Send + Sync + 'static,
RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
TCR: TransactionCounterTrait + Send + Sync + 'static,
PR: PluginRepositoryTrait + Send + Sync + 'static,
AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
{
let api_keys = state.api_key_repository.list_paginated(query).await?;

let api_key_items: Vec<ApiKeyRepoModel> = api_keys.items.into_iter().collect();

// Subtract the "value" from the api key to avoid exposing it.
let api_key_items: Vec<ApiKeyResponse> = api_key_items
.into_iter()
.map(ApiKeyResponse::try_from)
.collect::<Result<Vec<ApiKeyResponse>, ApiError>>()?;

Ok(HttpResponse::Ok().json(ApiResponse::paginated(
api_key_items,
PaginationMeta {
total_items: api_keys.total,
current_page: api_keys.page,
per_page: api_keys.per_page,
},
)))
}

/// Get api key permissions
///
/// # Arguments
///
/// * `api_key_id` - The id of the api key.
/// * `state` - The application state containing the api key repository.
///
pub async fn get_api_key_permissions<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
Comment thread
MCarlomagno marked this conversation as resolved.
api_key_id: String,
state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
) -> Result<HttpResponse, ApiError>
where
J: JobProducerTrait + Send + Sync + 'static,
RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
TCR: TransactionCounterTrait + Send + Sync + 'static,
PR: PluginRepositoryTrait + Send + Sync + 'static,
AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
{
let permissions = state
.api_key_repository
.list_permissions(&api_key_id)
.await?;

Ok(HttpResponse::Ok().json(ApiResponse::success(permissions)))
}

/// Delete api key
///
/// # Arguments
///
/// * `api_key_id` - The id of the api key.
/// * `state` - The application state containing the api key repository.
///
pub async fn delete_api_key<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
Comment thread
zeljkoX marked this conversation as resolved.
_api_key_id: String,
_state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
) -> Result<HttpResponse, ApiError>
where
J: JobProducerTrait + Send + Sync + 'static,
RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
TCR: TransactionCounterTrait + Send + Sync + 'static,
PR: PluginRepositoryTrait + Send + Sync + 'static,
AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
{
// state.api_key_repository.delete_by_id(&api_key_id).await?;

// Ok(HttpResponse::Ok().json(ApiResponse::success(api_key_id)))
Ok(HttpResponse::Ok().json(ApiResponse::<String>::error("Not implemented".to_string())))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{
models::{ApiKeyRepoModel, PaginationQuery, SecretString},
utils::mocks::mockutils::create_mock_app_state,
};
use actix_web::web::ThinData;

/// Helper function to create a test api key model
fn create_test_api_key_model(id: &str) -> ApiKeyRepoModel {
ApiKeyRepoModel {
id: id.to_string(),
value: SecretString::new("test-api-key-value"),
name: "Test API Key".to_string(),
allowed_origins: vec!["*".to_string()],
permissions: vec!["relayer:all:execute".to_string()],
created_at: "2023-01-01T00:00:00Z".to_string(),
}
}

/// Helper function to create a test api key create request
fn create_test_api_key_create_request(name: &str) -> ApiKeyRequest {
ApiKeyRequest {
name: name.to_string(),
permissions: vec!["relayer:all:execute".to_string()],
allowed_origins: Some(vec!["*".to_string()]),
}
}

#[actix_web::test]
async fn test_create_api_key() {
let app_state = create_mock_app_state(None, None, None, None, None, None).await;
let api_key_request = create_test_api_key_create_request("Test API Key");

let result = create_api_key(api_key_request, ThinData(app_state)).await;

assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), 201);
}

#[actix_web::test]
async fn test_list_api_keys_empty() {
let app_state = create_mock_app_state(None, None, None, None, None, None).await;
let query = PaginationQuery {
page: 1,
per_page: 10,
};

let result = list_api_keys(query, ThinData(app_state)).await;

assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), 200);
}

#[actix_web::test]
async fn test_list_api_keys_with_data() {
let api_key = create_test_api_key_model("test-api-key-1");
let app_state =
create_mock_app_state(Some(vec![api_key]), None, None, None, None, None).await;
let query = PaginationQuery {
page: 1,
per_page: 10,
};

let result = list_api_keys(query, ThinData(app_state)).await;

assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), 200);
}

#[actix_web::test]
async fn test_get_api_key_permissions() {
let api_key = create_test_api_key_model("test-api-key-1");
let api_key_id = api_key.id.clone();
let app_state =
create_mock_app_state(Some(vec![api_key]), None, None, None, None, None).await;

let result = get_api_key_permissions(api_key_id, ThinData(app_state)).await;

assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), 200);
}

// #[actix_web::test]
// async fn test_delete_api_key() {
// let api_key = create_test_api_key_model("test-api-key-1");
// let api_key_id = api_key.id.clone();
// let app_state =
// create_mock_app_state(Some(vec![api_key]), None, None, None, None, None).await;

// let result = delete_api_key(api_key_id, ThinData(app_state)).await;

// assert!(result.is_ok());
// let response = result.unwrap();
// assert_eq!(response.status(), 200);
// }

#[actix_web::test]
async fn test_get_permissions_nonexistent_api_key() {
let app_state = create_mock_app_state(None, None, None, None, None, None).await;

let result =
get_api_key_permissions("nonexistent-id".to_string(), ThinData(app_state)).await;

assert!(result.is_err());
}
}
2 changes: 2 additions & 0 deletions src/api/controllers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
//!
//! ## Controllers
//!
//! * `api_key` - API key management endpoints
//! * `relayer` - Transaction and relayer management endpoints
//! * `plugin` - Plugin endpoints
//! * `notifications` - Notification management endpoints
//! * `signers` - Signer management endpoints

pub mod api_key;
pub mod notification;
pub mod plugin;
pub mod relayer;
Expand Down
Loading
Loading