TUI:
- Split 1,756-line app.rs monolith into 7 modules (types, draw, commands, input, file_transfer, network, mod)
- Message timestamps [HH:MM], scrolling (PageUp/Down/arrows), connection status dot, unread badge
- /help command, terminal bell on incoming DM, /devices + /kick commands
- 44 unit tests (types, input, draw with TestBackend)
Server — WZP Call Infrastructure (FC-2/3/5/6/7/10):
- Call state management (CallState, CallStatus, active_calls, calls + missed_calls sled trees)
- WS call signal awareness (Offer/Answer/Hangup update state, missed call on offline)
- Group call endpoint (POST /groups/:name/call with SHA-256 room ID, fan-out)
- Presence API (GET /presence/:fp, POST /presence/batch)
- Missed call flush on WS reconnect
- WZP relay config + CORS
Server — Security (FC-P1):
- Auth enforcement middleware (AuthFingerprint extractor on 13 write handlers)
- Session auto-recovery (delete corrupted ratchet, show [session reset])
- WS connection cap (5/fingerprint) + global concurrency limit (200)
- Device management (GET /devices, POST /devices/:id/kick, POST /devices/revoke-all)
Server — Federation:
- Two-server federation via JSON config (--federation flag)
- Periodic presence sync (every 5s, full-state, self-healing)
- Message forwarding via HTTP POST with SHA-256(secret||body) auth
- Graceful degradation (peer down = queue locally)
- deliver_or_queue() replaces push-or-queue in ws.rs + messages.rs
Client — Group Messaging:
- SenderKeyDistribution storage + GroupSenderKey decryption in TUI
- sender_keys sled tree in LocalDb
WASM:
- All 8 WireMessage variants handled (no more "unsupported")
- decrypt_group_message() + create_sender_key_from_distribution() exports
- CallSignal parsing with signal_type mapping
Docs:
- ARCHITECTURE.md rewritten with Mermaid diagrams
- README.md created
- TASK_PLAN.md with FC-P{phase}-T{task} naming
- PROGRESS.md updated to v0.0.21
WZP submodule updated to 6f4e8eb (IAX2 trunking, adaptive quality, metrics, all S-tasks done)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
85 lines
2.6 KiB
Rust
85 lines
2.6 KiB
Rust
//! Auth enforcement middleware: axum extractor that validates bearer tokens.
|
|
//!
|
|
//! Reads `Authorization: Bearer <token>` from request headers, validates via
|
|
//! [`crate::routes::auth::validate_token`], and returns the authenticated
|
|
//! fingerprint or a 401 rejection.
|
|
|
|
use axum::{
|
|
extract::FromRequestParts,
|
|
http::{request::Parts, StatusCode},
|
|
response::{IntoResponse, Response},
|
|
};
|
|
|
|
use crate::state::AppState;
|
|
|
|
/// Extractor that validates a bearer token and provides the authenticated fingerprint.
|
|
///
|
|
/// Place this as the **first** parameter in any handler that requires authentication.
|
|
/// The extractor will reject the request with 401 if the token is missing or invalid.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// async fn my_handler(
|
|
/// auth: AuthFingerprint,
|
|
/// State(state): State<AppState>,
|
|
/// ) -> impl IntoResponse {
|
|
/// let fp = auth.fingerprint; // guaranteed valid
|
|
/// // ...
|
|
/// }
|
|
/// ```
|
|
pub struct AuthFingerprint {
|
|
pub fingerprint: String,
|
|
}
|
|
|
|
#[axum::async_trait]
|
|
impl FromRequestParts<AppState> for AuthFingerprint {
|
|
type Rejection = AuthError;
|
|
|
|
async fn from_request_parts(
|
|
parts: &mut Parts,
|
|
state: &AppState,
|
|
) -> Result<Self, Self::Rejection> {
|
|
let header = parts
|
|
.headers
|
|
.get("authorization")
|
|
.and_then(|v| v.to_str().ok())
|
|
.and_then(|s| s.strip_prefix("Bearer "))
|
|
.map(|s| s.trim().to_string());
|
|
|
|
let token = match header {
|
|
Some(t) if !t.is_empty() => t,
|
|
_ => return Err(AuthError::MissingToken),
|
|
};
|
|
|
|
match crate::routes::auth::validate_token(&state.db.tokens, &token) {
|
|
Some(fingerprint) => Ok(AuthFingerprint { fingerprint }),
|
|
None => Err(AuthError::InvalidToken),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rejection type for [`AuthFingerprint`] extractor failures.
|
|
pub enum AuthError {
|
|
/// No `Authorization: Bearer <token>` header was present (or it was empty).
|
|
MissingToken,
|
|
/// The token was present but did not pass validation (expired or unknown).
|
|
InvalidToken,
|
|
}
|
|
|
|
impl IntoResponse for AuthError {
|
|
fn into_response(self) -> Response {
|
|
let (status, msg) = match self {
|
|
AuthError::MissingToken => (
|
|
StatusCode::UNAUTHORIZED,
|
|
"missing or empty Authorization: Bearer <token> header",
|
|
),
|
|
AuthError::InvalidToken => (
|
|
StatusCode::UNAUTHORIZED,
|
|
"invalid or expired token",
|
|
),
|
|
};
|
|
(status, axum::Json(serde_json::json!({ "error": msg }))).into_response()
|
|
}
|
|
}
|