Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .github/agents/committer.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Treat every commit request as a review-and-verify workflow, not as a blind reque
and retry with `./contrib/dev-tools/git/hooks/pre-commit.sh --format=text --verbosity=verbose`
when deeper diagnostics are needed.
- Create GPG-signed Conventional Commits (`git commit -S`).
**GPG timeout handling**: If a `git commit -S` invocation fails because the GPG passphrase
prompt timed out, stop immediately and notify the user. Do not retry with `--no-gpg-sign`,
do not amend the commit without a signature, and do not proceed with the work. The user
must manually retry the commit to provide the passphrase.

## Required Workflow

Expand Down
11 changes: 11 additions & 0 deletions .github/skills/dev/git-workflow/commit-changes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ Scope should reflect the affected package or area (e.g., `tracker-core`, `udp-pr
git commit -S -m "your commit message"
```

### GPG Timeout Handling

If the GPG passphrase prompt times out (`gpg: signing failed: Timeout`), the agent **must**:

1. **Stop immediately.** Do not retry, do not use `--no-gpg-sign`, do not skip signing.
2. **Notify the user.** Tell them the GPG passphrase prompt timed out and they need to retry
the commit manually by running the same `git commit -S` command themselves.
3. **Wait for the user** to confirm the commit was completed before proceeding.

This rule is absolute. Never bypass GPG signing for any reason.

## Pre-commit Verification (MANDATORY)

### Git Hook
Expand Down
16 changes: 9 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,21 +269,23 @@ Implementation workflow references:
## 🔧 Essential Rules

1. **Linting gate**: `linter all` must exit `0` before every commit. No exceptions.
2. **GPG commit signing**: All commits **must** be signed with GPG (`git commit -S`).
3. **Never commit `storage/` or `target/`**: These directories contain runtime data and build
2. **GPG commit signing**: All commits **must** be signed with GPG (`git commit -S`). **GPG timeout handling**: If the GPG passphrase prompt times out during a commit, the agent
**must stop immediately**, notify the user, and ask them to retry the commit manually.
Never bypass GPG signing with `--no-gpg-sign` or skip the signing step under any
circumstances. This rule is absolute.3. **Never commit `storage/` or `target/`**: These directories contain runtime data and build
artifacts. They are git-ignored; never force-add them.
4. **Unused dependencies**: Run `cargo machete` before committing. Remove any unused
3. **Unused dependencies**: Run `cargo machete` before committing. Remove any unused
dependencies immediately.
5. **Rust imports**: All imports at the top of the file, grouped (std → external crates →
4. **Rust imports**: All imports at the top of the file, grouped (std → external crates →
internal crate). Prefer short imported names over fully-qualified paths.
6. **Continuous self-review**: Review your own work against project quality standards. Apply
5. **Continuous self-review**: Review your own work against project quality standards. Apply
self-review at three levels:
- **Mandatory** — before opening a pull request
- **Strongly recommended** — before each commit
- **Recommended** — after completing each small, independent, deployable change
7. **Security**: Do not report security vulnerabilities through public GitHub issues. Send an
6. **Security**: Do not report security vulnerabilities through public GitHub issues. Send an
email to `info@nautilus-cyberneering.de` instead. See [SECURITY.md](SECURITY.md).
8. **Skill-link synchronization**: When modifying any artifact containing a `skill-link:` marker,
7. **Skill-link synchronization**: When modifying any artifact containing a `skill-link:` marker,
also review and update the linked skill instructions in `.github/skills/` so behavior,
commands, and references remain aligned. If the linked skill has a validation script, run it
before finishing.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 28 additions & 23 deletions docs/issues/open/1941-1938-si-3-migrate-auth-key-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The context has locally-defined DTOs (`AuthKey`, `AddKeyForm`, `KeyParam`) and 7

- Move `AuthKey`, `AddKeyForm`, `KeyParam` DTOs to `rest-api-protocol/src/v1/context/auth_key/resources/auth_key.rs`.
- Add auth-key-specific response/error DTOs to protocol (or reuse `ActionStatus` where applicable).
- Define `AuthKeyCommandPort` trait in `rest-api-application/src/ports/`.
- Define `AuthKeyPort` trait in `rest-api-application/src/ports/`.
- Implement `AuthKeyApiService` use-case in `rest-api-application/src/use_cases/`.
- Implement `TrackerAuthKeyAdapter` in `rest-api-runtime-adapter/src/adapters/`.
- Add conversion functions for domain→protocol types.
Expand All @@ -71,14 +71,18 @@ The context has locally-defined DTOs (`AuthKey`, `AddKeyForm`, `KeyParam`) and 7

The auth key context has both command and query operations, and includes form validation (duration parsing via `clock`). The 7 response functions produce 4 distinct error types plus a success response. Some can be consolidated into protocol-level error codes.

All protocol DTOs follow the normalized context-based module structure under `packages/rest-api-protocol/src/v1/context/`:
All protocol types follow the normalized context-based module structure under `packages/rest-api-protocol/src/v1/context/`.
Each context can have a `forms/` subdirectory alongside `resources/` for input DTOs:

```text
context/auth_key/
├── mod.rs # pub mod resources;
├── mod.rs # pub mod forms; pub mod resources;
├── forms/
│ ├── mod.rs # pub mod add_key_form;
│ └── add_key_form.rs # AddKeyForm input DTO
└── resources/
├── mod.rs # pub mod auth_key;
└── auth_key.rs # AuthKey, AddKeyForm, DTOs
└── auth_key.rs # AuthKey, AuthKeyError
```

Ports, use-cases, and adapters are flat files named after the context:
Expand All @@ -99,30 +103,31 @@ See the `torrent` and `health_check` contexts for the reference pattern.

## Implementation Plan

| ID | Status | Task | Notes |
| --- | ------ | ---------------------------------------------------------------------------------------------------------- | ----------------------------- |
| T1 | TODO | Add `auth_key` context module to `rest-api-protocol/src/v1/context/` with `AuthKey` DTO (resources subdir) | |
| T2 | TODO | Add `AddKeyRequest` DTO to protocol (or reuse from domain with wrapper) | |
| T3 | TODO | Add auth-key error response types to protocol | |
| T4 | TODO | Define `AuthKeyCommandPort` in `rest-api-application/src/ports/` | Methods for CRUD + reload |
| T5 | TODO | Implement `AuthKeyApiService` in `rest-api-application/src/use_cases/` | |
| T6 | TODO | Implement `TrackerAuthKeyAdapter` in `rest-api-runtime-adapter/src/adapters/` | Wraps `KeysHandler` + `clock` |
| T7 | TODO | Update Axum handlers to use `AuthKeyApiService` | |
| T8 | TODO | Update Axum state/routes to wire the new adapter | |
| T9 | TODO | Verify pre-commit and pre-push checks pass | |
| ID | Status | Task | Notes |
| --- | ------ | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| T1 | DONE | Add `auth_key` context module to `rest-api-protocol/src/v1/context/` with `AuthKey` DTO (resources subdir) | |
| T2 | DONE | Add `AddKeyForm` input DTO to protocol (forms/ subdir) | `AddKeyForm` moved to protocol `forms/` |
| T3 | DONE | Add `AuthKeyError` response types to protocol | 3-variant enum matching `PeerKeyError` |
| T4 | DONE | Define `AuthKeyPort` in `rest-api-application/src/ports/` | Methods for add, generate, delete, reload |
| T5 | DONE | Implement `AuthKeyApiService` in `rest-api-application/src/use_cases/` | |
| T6 | DONE | Implement `TrackerAuthKeyAdapter` in `rest-api-runtime-adapter/src/adapters/` | Wraps `KeysHandler` + `peer_key_to_auth_key` fn |
| T7 | DONE | Update Axum handlers to use `AuthKeyApiService` | |
| T8 | DONE | Update Axum state/routes to wire the new adapter | In `v1/routes.rs` |
| T9 | DONE | Verify pre-commit and pre-push checks pass | Pre-commit passed |

## Verification / Progress

- [ ] Protocol DTOs created and exported
- [ ] `AuthKeyCommandPort` trait defined in `rest-api-application`
- [ ] `AuthKeyApiService` use-case implemented
- [ ] `TrackerAuthKeyAdapter` implemented in `rest-api-runtime-adapter`
- [ ] Axum handlers dispatch through use-case
- [x] Protocol DTOs created and exported (resources + forms)
- [x] `AuthKeyPort` trait defined in `rest-api-application`
- [x] `AuthKeyApiService` use-case implemented
- [x] `TrackerAuthKeyAdapter` implemented in `rest-api-runtime-adapter`
- [x] Axum handlers dispatch through use-case
- [ ] Pre-commit checks pass
- [ ] Pre-push checks pass

### Progress Log

| Date | Event |
| ---------- | ------------------ |
| 2026-06-24 | Draft spec created |
| Date | Event |
| ---------- | -------------------------------------------------------- |
| 2026-06-24 | Draft spec created |
| 2026-06-26 | Auth key context migrated to contract-first architecture |
1 change: 0 additions & 1 deletion packages/axum-rest-api-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ hyper = "1"
reqwest = { version = "0", features = [ "json" ] }
serde = { version = "1", features = [ "derive" ] }
serde_json = { version = "1", features = [ "preserve_order" ] }
serde_with = { version = "3", features = [ "json" ] }
thiserror = "2"
tokio = { version = "1", features = [ "macros", "net", "rt-multi-thread", "signal", "sync" ] }
torrust-tracker-axum-server = { version = "3.0.0-develop", path = "../axum-server" }
Expand Down
101 changes: 50 additions & 51 deletions packages/axum-rest-api-server/src/v1/context/auth_key/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
//! API handlers for the [`auth_key`](crate::v1::context::auth_key) API context.
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

use axum::extract::{self, Path, State};
use axum::response::Response;
use serde::Deserialize;
use torrust_tracker_core::authentication::Key;
use torrust_tracker_core::authentication::handler::{AddKeyRequest, KeysHandler};
use torrust_tracker_rest_api_application::use_cases::auth_key::AuthKeyApiService;
use torrust_tracker_rest_api_protocol::v1::context::auth_key::forms::add_key_form::AddKeyForm;

use super::forms::AddKeyForm;
use super::responses::{
auth_key_response, failed_to_delete_key_response, failed_to_generate_key_response, failed_to_reload_keys_response,
invalid_auth_key_duration_response, invalid_auth_key_response,
auth_key_response, failed_to_add_key_response, failed_to_delete_key_response, failed_to_generate_key_response,
failed_to_reload_keys_response, invalid_auth_key_duration_response, invalid_auth_key_response,
};
use crate::v1::context::auth_key::resources::AuthKey;
use crate::v1::responses::{invalid_auth_key_param_response, ok_response};

/// It handles the request to add a new authentication key.
Expand All @@ -31,23 +27,22 @@ use crate::v1::responses::{invalid_auth_key_param_response, ok_response};
/// Refer to the [API endpoint documentation](crate::v1::context::auth_key#generate-a-new-authentication-key)
/// for more information about this endpoint.
pub async fn add_auth_key_handler(
State(keys_handler): State<Arc<KeysHandler>>,
State(auth_key_service): State<Arc<AuthKeyApiService>>,
extract::Json(add_key_form): extract::Json<AddKeyForm>,
) -> Response {
match keys_handler
.add_peer_key(AddKeyRequest {
opt_key: add_key_form.opt_key.clone(),
opt_seconds_valid: add_key_form.opt_seconds_valid,
})
.await
{
Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)),
Err(err) => match err {
torrust_tracker_core::error::PeerKeyError::DurationOverflow { seconds_valid } => {
invalid_auth_key_duration_response(seconds_valid)
match auth_key_service.add_key(&add_key_form).await {
Ok(auth_key) => auth_key_response(&auth_key),
Err(err) => match &err {
torrust_tracker_rest_api_protocol::v1::context::auth_key::resources::auth_key::AuthKeyError::DurationOverflow {
seconds_valid,
} => invalid_auth_key_duration_response(*seconds_valid),
torrust_tracker_rest_api_protocol::v1::context::auth_key::resources::auth_key::AuthKeyError::InvalidKey {
key,
reason,
} => invalid_auth_key_response(key, reason),
torrust_tracker_rest_api_protocol::v1::context::auth_key::resources::auth_key::AuthKeyError::Database(_) => {
failed_to_add_key_response(AuthKeyErrorDisplay(&err))
}
torrust_tracker_core::error::PeerKeyError::InvalidKey { key, source } => invalid_auth_key_response(&key, source),
torrust_tracker_core::error::PeerKeyError::DatabaseError { source } => failed_to_generate_key_response(source),
},
}
}
Expand All @@ -66,34 +61,19 @@ pub async fn add_auth_key_handler(
///
/// This endpoint has been deprecated. Use [`add_auth_key_handler`].
pub async fn generate_auth_key_handler(
State(keys_handler): State<Arc<KeysHandler>>,
State(auth_key_service): State<Arc<AuthKeyApiService>>,
Path(seconds_valid_or_key): Path<u64>,
) -> Response {
let seconds_valid = seconds_valid_or_key;
match keys_handler
.generate_expiring_peer_key(Some(Duration::from_secs(seconds_valid)))
.await
{
Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)),
Err(e) => failed_to_generate_key_response(e),
match auth_key_service.generate_key(seconds_valid).await {
Ok(auth_key) => auth_key_response(&auth_key),
Err(e) => failed_to_generate_key_response(AuthKeyErrorDisplay(&e)),
}
}

/// A container for the `key` parameter extracted from the URL PATH.
///
/// It does not perform any validation, it just stores the value.
///
/// In the current API version, the `key` parameter can be either a valid key
/// like `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6` or the number of seconds the
/// key will be valid, for example two minutes `120`.
///
/// For example, the `key` is used in the following requests:
///
/// - `POST /api/v1/key/120`. It will generate a new key valid for two minutes.
/// - `DELETE /api/v1/key/xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. It will delete the
/// key `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`.
///
/// > **NOTICE**: this may change in the future, in the [API v2](https://github.com/torrust/torrust-tracker/issues/144).
#[derive(Deserialize)]
pub struct KeyParam(String);

Expand All @@ -109,15 +89,16 @@ pub struct KeyParam(String);
/// Refer to the [API endpoint documentation](crate::v1::context::auth_key#delete-an-authentication-key)
/// for more information about this endpoint.
pub async fn delete_auth_key_handler(
State(keys_handler): State<Arc<KeysHandler>>,
State(auth_key_service): State<Arc<AuthKeyApiService>>,
Path(seconds_valid_or_key): Path<KeyParam>,
) -> Response {
match Key::from_str(&seconds_valid_or_key.0) {
Err(_) => invalid_auth_key_param_response(&seconds_valid_or_key.0),
Ok(key) => match keys_handler.remove_peer_key(&key).await {
Ok(()) => ok_response(),
Err(e) => failed_to_delete_key_response(e),
},
match auth_key_service.delete_key(&seconds_valid_or_key.0).await {
Ok(()) => ok_response(),
Err(torrust_tracker_rest_api_protocol::v1::context::auth_key::resources::auth_key::AuthKeyError::InvalidKey {
key: _,
reason: _,
}) => invalid_auth_key_param_response(&seconds_valid_or_key.0),
Err(e) => failed_to_delete_key_response(AuthKeyErrorDisplay(&e)),
}
}

Expand All @@ -133,9 +114,27 @@ pub async fn delete_auth_key_handler(
///
/// Refer to the [API endpoint documentation](crate::v1::context::auth_key#reload-authentication-keys)
/// for more information about this endpoint.
pub async fn reload_keys_handler(State(keys_handler): State<Arc<KeysHandler>>) -> Response {
match keys_handler.load_peer_keys_from_database().await {
pub async fn reload_keys_handler(State(auth_key_service): State<Arc<AuthKeyApiService>>) -> Response {
match auth_key_service.reload_keys().await {
Ok(()) => ok_response(),
Err(e) => failed_to_reload_keys_response(e),
Err(e) => failed_to_reload_keys_response(AuthKeyErrorDisplay(&e)),
}
}

/// Wrapper to allow passing an [`AuthKeyError`] reference to response
/// functions that expect `E: std::error::Error`.
struct AuthKeyErrorDisplay<'a>(&'a torrust_tracker_rest_api_protocol::v1::context::auth_key::resources::auth_key::AuthKeyError);

impl std::fmt::Display for AuthKeyErrorDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self.0, f)
}
}

impl std::error::Error for AuthKeyErrorDisplay<'_> {}

impl std::fmt::Debug for AuthKeyErrorDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self.0, f)
}
}
2 changes: 0 additions & 2 deletions packages/axum-rest-api-server/src/v1/context/auth_key/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,6 @@
//! "status": "ok"
//! }
//! ```
pub mod forms;
pub mod handlers;
pub mod resources;
pub mod responses;
pub mod routes;
Loading
Loading