fmt 2021 ->2024 rfc0034
EBO <eric.bouhana@softalys.com> committé le 2026-05-18 22:15
c6551853a5c97b59b01f337ff4f5d537ea4c5ce0
1 parent(s)
83 fichiers modifiés
+743
-905
M
app/examples/minimal_headless.rs
+1
-1
@@ -6,7 +6,7 @@
| 6 | 6 | //! cargo run --example minimal_headless |
| 7 | 7 | //! ``` |
| 8 | 8 | |
| 9 | -use axum::{routing::get, Json, Router}; | |
| 9 | +use axum::{Json, Router, routing::get}; | |
| 10 | 10 | use rustwarden_core::prelude::*; |
| 11 | 11 | use sea_orm::DatabaseConnection; |
| 12 | 12 | use serde_json::json; |
M
app/src/main.rs
+14
-21
@@ -39,9 +39,8 @@ async fn main() -> anyhow::Result<()> {
| 39 | 39 | // `AlwaysAcceptVerifier::with_resolver` so the e2e suite can |
| 40 | 40 | // exercise the signed-request happy path without the |
| 41 | 41 | // testbed pre-seeding every actor's PEM into each peer's cache. |
| 42 | - let resolver: Arc<dyn rustwarden_fediverse::resolver::LocalUserResolver> = Arc::new( | |
| 43 | - rustwarden_fediverse::resolver::SeaOrmLocalUserResolver::new(db.clone()), | |
| 44 | - ); | |
| 42 | + let resolver: Arc<dyn rustwarden_fediverse::resolver::LocalUserResolver> = | |
| 43 | + Arc::new(rustwarden_fediverse::resolver::SeaOrmLocalUserResolver::new(db.clone())); | |
| 45 | 44 | let debug_local = std::env::var("FEDIVERSE_DEBUG_LOCAL_FEDERATION") |
| 46 | 45 | .map(|v| matches!(v.to_ascii_lowercase().as_str(), "true" | "1" | "yes")) |
| 47 | 46 | .unwrap_or(false); |
@@ -58,9 +57,7 @@ async fn main() -> anyhow::Result<()> {
| 58 | 57 | ) |
| 59 | 58 | } else { |
| 60 | 59 | Arc::new( |
| 61 | - rustwarden_fediverse::http_signatures::HttpSigVerifier::with_resolver( | |
| 62 | - db.clone(), | |
| 63 | - ), | |
| 60 | + rustwarden_fediverse::http_signatures::HttpSigVerifier::with_resolver(db.clone()), | |
| 64 | 61 | ) |
| 65 | 62 | }; |
| 66 | 63 | let fediverse_router = rustwarden_fediverse::routes::router_with_auth( |
@@ -84,14 +81,13 @@ async fn main() -> anyhow::Result<()> {
| 84 | 81 | let fediverse_router = { |
| 85 | 82 | use rustwarden_fediverse::observer::FediverseEventObserver; |
| 86 | 83 | use rustwarden_fediverse_guard::{ |
| 87 | - default_detectors, BanManager, GuardConfig, GuardObserver, GuardStore, | |
| 88 | - HybridStore, PostgresStore, | |
| 84 | + BanManager, GuardConfig, GuardObserver, GuardStore, HybridStore, PostgresStore, | |
| 85 | + default_detectors, | |
| 89 | 86 | }; |
| 90 | 87 | |
| 91 | - let guard_config = GuardConfig::from_env() | |
| 92 | - .map_err(|e| anyhow::anyhow!("invalid GuardConfig: {e}"))?; | |
| 93 | - let postgres_store: Arc<dyn GuardStore> = | |
| 94 | - Arc::new(PostgresStore::new(db.clone())); | |
| 88 | + let guard_config = | |
| 89 | + GuardConfig::from_env().map_err(|e| anyhow::anyhow!("invalid GuardConfig: {e}"))?; | |
| 90 | + let postgres_store: Arc<dyn GuardStore> = Arc::new(PostgresStore::new(db.clone())); | |
| 95 | 91 | let hybrid = HybridStore::new(postgres_store); |
| 96 | 92 | // Repopulate the in-memory cache from durable storage before |
| 97 | 93 | // serving requests — bans active at boot must be enforced |
@@ -107,13 +103,11 @@ async fn main() -> anyhow::Result<()> {
| 107 | 103 | // Lot 8 — mount the guard admin UI alongside, sharing the |
| 108 | 104 | // same store + config as the runtime observer so every |
| 109 | 105 | // screen surfaces live data. |
| 110 | - fediverse_router | |
| 111 | - .layer(axum::Extension(observer)) | |
| 112 | - .merge( | |
| 113 | - rustwarden_fediverse_guard::ui::default_routes() | |
| 114 | - .layer(axum::Extension(hybrid_store)) | |
| 115 | - .layer(axum::Extension(guard_config)), | |
| 116 | - ) | |
| 106 | + fediverse_router.layer(axum::Extension(observer)).merge( | |
| 107 | + rustwarden_fediverse_guard::ui::default_routes() | |
| 108 | + .layer(axum::Extension(hybrid_store)) | |
| 109 | + .layer(axum::Extension(guard_config)), | |
| 110 | + ) | |
| 117 | 111 | }; |
| 118 | 112 | |
| 119 | 113 | let checker: Arc< |
@@ -131,8 +125,7 @@ async fn main() -> anyhow::Result<()> {
| 131 | 125 | .merge_routes(fediverse_router); |
| 132 | 126 | |
| 133 | 127 | #[cfg(feature = "fediverse-guard")] |
| 134 | - let builder = builder | |
| 135 | - .add_dashboard_card(rustwarden_fediverse_guard::ui::dashboard_card()); | |
| 128 | + let builder = builder.add_dashboard_card(rustwarden_fediverse_guard::ui::dashboard_card()); | |
| 136 | 129 | |
| 137 | 130 | builder |
| 138 | 131 | }; |
M
crates/rustwarden-core/src/app.rs
+10
-10
@@ -19,11 +19,11 @@
| 19 | 19 | //! ``` |
| 20 | 20 | |
| 21 | 21 | use axum::{ |
| 22 | + Router, | |
| 22 | 23 | extract::Request, |
| 23 | - http::{header::HeaderName, HeaderValue, Method}, | |
| 24 | + http::{HeaderValue, Method, header::HeaderName}, | |
| 24 | 25 | middleware::Next, |
| 25 | 26 | response::Response, |
| 26 | - Router, | |
| 27 | 27 | }; |
| 28 | 28 | use sea_orm::DatabaseConnection; |
| 29 | 29 | use std::path::PathBuf; |
@@ -392,8 +392,8 @@ impl RustwardenBuilder {
| 392 | 392 | let oauth_registry = { |
| 393 | 393 | use crate::config::oauth::OAuthConfig; |
| 394 | 394 | use crate::services::oauth::providers::{ |
| 395 | - discord::DiscordProvider, github::GitHubProvider, google::GoogleProvider, | |
| 396 | - microsoft::MicrosoftProvider, ProviderRegistry, | |
| 395 | + ProviderRegistry, discord::DiscordProvider, github::GitHubProvider, | |
| 396 | + google::GoogleProvider, microsoft::MicrosoftProvider, | |
| 397 | 397 | }; |
| 398 | 398 | |
| 399 | 399 | let oauth_config = OAuthConfig::load(&db).await; |
@@ -523,9 +523,9 @@ impl RustwardenBuilder {
| 523 | 523 | .collect(); |
| 524 | 524 | if origins.is_empty() { |
| 525 | 525 | tracing::warn!("CORS_ALLOWED_ORIGINS set but no valid origins parsed"); |
| 526 | - CorsLayer::new().allow_origin(AllowOrigin::list( | |
| 527 | - std::iter::empty::<axum::http::HeaderValue>(), | |
| 528 | - )) | |
| 526 | + CorsLayer::new().allow_origin(AllowOrigin::list(std::iter::empty::< | |
| 527 | + axum::http::HeaderValue, | |
| 528 | + >())) | |
| 529 | 529 | } else { |
| 530 | 530 | tracing::info!("CORS configured for {} origins", origins.len()); |
| 531 | 531 | CorsLayer::new() |
@@ -546,9 +546,9 @@ impl RustwardenBuilder {
| 546 | 546 | .max_age(std::time::Duration::from_secs(3600)) |
| 547 | 547 | } |
| 548 | 548 | } else { |
| 549 | - CorsLayer::new().allow_origin(AllowOrigin::list( | |
| 550 | - std::iter::empty::<axum::http::HeaderValue>(), | |
| 551 | - )) | |
| 549 | + CorsLayer::new().allow_origin(AllowOrigin::list(std::iter::empty::< | |
| 550 | + axum::http::HeaderValue, | |
| 551 | + >())) | |
| 552 | 552 | }; |
| 553 | 553 | |
| 554 | 554 | let protected_prefixes = std::sync::Arc::new(self.protected_prefixes.clone()); |
M
crates/rustwarden-core/src/auth/extractors.rs
+1
-1
@@ -6,7 +6,7 @@
| 6 | 6 | |
| 7 | 7 | use axum::{ |
| 8 | 8 | extract::FromRequestParts, |
| 9 | - http::{request::Parts, StatusCode}, | |
| 9 | + http::{StatusCode, request::Parts}, | |
| 10 | 10 | }; |
| 11 | 11 | use sea_orm::DatabaseConnection; |
| 12 | 12 | use std::sync::Arc; |
M
crates/rustwarden-core/src/auth/handlers.rs
+1
-1
@@ -8,10 +8,10 @@
| 8 | 8 | |
| 9 | 9 | use askama::Template; |
| 10 | 10 | use axum::{ |
| 11 | + Extension, Json, | |
| 11 | 12 | extract::{Path, State}, |
| 12 | 13 | http::HeaderMap, |
| 13 | 14 | response::{Html, IntoResponse, Response}, |
| 14 | - Extension, Json, | |
| 15 | 15 | }; |
| 16 | 16 | use axum_extra::extract::CookieJar; |
| 17 | 17 | use chrono::{DateTime, Utc}; |
M
crates/rustwarden-core/src/auth/jwt.rs
+1
-1
@@ -6,7 +6,7 @@
| 6 | 6 | //! - Token expiration management |
| 7 | 7 | |
| 8 | 8 | use chrono::{DateTime, Utc}; |
| 9 | -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; | |
| 9 | +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; | |
| 10 | 10 | use serde::{Deserialize, Serialize}; |
| 11 | 11 | use uuid::Uuid; |
| 12 | 12 |
M
crates/rustwarden-core/src/auth/oauth_handlers.rs
+2
-2
@@ -1,8 +1,8 @@
| 1 | 1 | use axum::{ |
| 2 | + Extension, Json, | |
| 2 | 3 | extract::{Path, Query, State}, |
| 3 | 4 | http::{HeaderMap, StatusCode}, |
| 4 | 5 | response::{IntoResponse, Redirect, Response}, |
| 5 | - Extension, Json, | |
| 6 | 6 | }; |
| 7 | 7 | use sea_orm::DatabaseConnection; |
| 8 | 8 | use serde::Deserialize; |
@@ -14,8 +14,8 @@ use crate::config::auth::AuthConfig;
| 14 | 14 | use crate::config::oauth::OAuthConfig; |
| 15 | 15 | use crate::error::AppError; |
| 16 | 16 | use crate::hooks::RustwardenHooks; |
| 17 | -use crate::services::oauth::providers::ProviderRegistry; | |
| 18 | 17 | use crate::services::oauth::OAuthService; |
| 18 | +use crate::services::oauth::providers::ProviderRegistry; | |
| 19 | 19 | use crate::services::refresh_token_service::RefreshTokenService; |
| 20 | 20 | use crate::services::user_preferences_service::UserPreferencesService; |
| 21 | 21 | use crate::services::user_service::UserService; |
M
crates/rustwarden-core/src/error.rs
+8
-6
@@ -1,7 +1,7 @@
| 1 | 1 | use axum::{ |
| 2 | + Json, | |
| 2 | 3 | http::StatusCode, |
| 3 | 4 | response::{IntoResponse, Response}, |
| 4 | - Json, | |
| 5 | 5 | }; |
| 6 | 6 | use serde_json::json; |
| 7 | 7 | use thiserror::Error; |
@@ -290,10 +290,12 @@ mod tests {
| 290 | 290 | let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); |
| 291 | 291 | |
| 292 | 292 | assert_eq!(body["status"], 429); |
| 293 | - assert!(body["error"] | |
| 294 | - .as_str() | |
| 295 | - .unwrap() | |
| 296 | - .to_lowercase() | |
| 297 | - .contains("too many")); | |
| 293 | + assert!( | |
| 294 | + body["error"] | |
| 295 | + .as_str() | |
| 296 | + .unwrap() | |
| 297 | + .to_lowercase() | |
| 298 | + .contains("too many") | |
| 299 | + ); | |
| 298 | 300 | } |
| 299 | 301 | } |
M
crates/rustwarden-core/src/i18n/extractor.rs
+1
-1
@@ -5,7 +5,7 @@
| 5 | 5 | |
| 6 | 6 | use axum::{ |
| 7 | 7 | extract::FromRequestParts, |
| 8 | - http::{request::Parts, StatusCode}, | |
| 8 | + http::{StatusCode, request::Parts}, | |
| 9 | 9 | }; |
| 10 | 10 | |
| 11 | 11 | /// User locale extracted from request |
M
crates/rustwarden-core/src/identity.rs
+1
-4
@@ -96,10 +96,7 @@ pub trait IdentityResolver: Send + Sync {
| 96 | 96 | |
| 97 | 97 | /// Returns `Some(Identity::Remote(remote_identity_id))` if the row |
| 98 | 98 | /// exists in `remote_identities`, `None` otherwise. |
| 99 | - async fn resolve_remote( | |
| 100 | - &self, | |
| 101 | - remote_identity_id: Uuid, | |
| 102 | - ) -> Result<Option<Identity>, AppError>; | |
| 99 | + async fn resolve_remote(&self, remote_identity_id: Uuid) -> Result<Option<Identity>, AppError>; | |
| 103 | 100 | } |
| 104 | 101 | |
| 105 | 102 | #[cfg(test)] |
M
crates/rustwarden-core/src/lib.rs
+6
-6
@@ -7,13 +7,17 @@
| 7 | 7 | pub mod app; |
| 8 | 8 | /// Authentication extractors, JWT service, and session management. |
| 9 | 9 | pub mod auth; |
| 10 | +/// Server, database, authentication, and email configuration. | |
| 11 | +pub mod config; | |
| 10 | 12 | /// Dashboard extension surface (`DashboardCardDTO`) used by companion |
| 11 | 13 | /// crates to inject extra cards into the admin dashboard. |
| 12 | 14 | pub mod dashboard; |
| 13 | -/// Server, database, authentication, and email configuration. | |
| 14 | -pub mod config; | |
| 15 | 15 | /// Application error types and HTTP response conversion. |
| 16 | 16 | pub mod error; |
| 17 | +/// Extension hooks for consumer applications to react to framework events. | |
| 18 | +pub mod hooks; | |
| 19 | +/// Internationalization utilities. | |
| 20 | +pub mod i18n; | |
| 17 | 21 | /// Unified actor abstraction (RFC 0008). |
| 18 | 22 | /// |
| 19 | 23 | /// `Identity` and `IdentityResolver` stay in core (no federation dep) |
@@ -22,10 +26,6 @@ pub mod error;
| 22 | 26 | /// `rustwarden-fediverse` crate is not linked. ActivityPub federation |
| 23 | 27 | /// itself moved to `rustwarden-fediverse` (RFC 0001+). |
| 24 | 28 | pub mod identity; |
| 25 | -/// Extension hooks for consumer applications to react to framework events. | |
| 26 | -pub mod hooks; | |
| 27 | -/// Internationalization utilities. | |
| 28 | -pub mod i18n; | |
| 29 | 29 | /// Axum middleware: authentication, CSRF, rate-limiting, error handling. |
| 30 | 30 | pub mod middleware; |
| 31 | 31 | /// SeaORM database migrations bundled with the core crate. |
M
crates/rustwarden-core/src/middleware/admin_required.rs
+2
-4
@@ -72,8 +72,7 @@ pub async fn admin_required(
| 72 | 72 | Some(claims) => claims, |
| 73 | 73 | None => { |
| 74 | 74 | tracing::warn!("Unauthenticated access attempt to admin route: {}", uri); |
| 75 | - let redirect_url = | |
| 76 | - crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 75 | + let redirect_url = crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 77 | 76 | return Redirect::to(&redirect_url).into_response(); |
| 78 | 77 | } |
| 79 | 78 | }; |
@@ -112,8 +111,7 @@ pub async fn admin_required_impl(
| 112 | 111 | Some(claims) => claims, |
| 113 | 112 | None => { |
| 114 | 113 | tracing::warn!("Unauthenticated access attempt to admin route: {}", uri); |
| 115 | - let redirect_url = | |
| 116 | - crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 114 | + let redirect_url = crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 117 | 115 | return Ok(Redirect::to(&redirect_url).into_response()); |
| 118 | 116 | } |
| 119 | 117 | }; |
M
crates/rustwarden-core/src/middleware/auth_required.rs
+6
-5
@@ -125,7 +125,10 @@ pub async fn auth_required_impl(
| 125 | 125 | ); |
| 126 | 126 | |
| 127 | 127 | // Load auth configuration from request extensions (injected at startup) |
| 128 | - let auth_config = request.extensions().get::<Arc<AuthConfig>>().cloned() | |
| 128 | + let auth_config = request | |
| 129 | + .extensions() | |
| 130 | + .get::<Arc<AuthConfig>>() | |
| 131 | + .cloned() | |
| 129 | 132 | .ok_or_else(|| { |
| 130 | 133 | tracing::error!("AuthConfig extension missing from request"); |
| 131 | 134 | AppError::Config("Auth configuration not available".to_string()) |
@@ -136,8 +139,7 @@ pub async fn auth_required_impl(
| 136 | 139 | Ok(token) => token, |
| 137 | 140 | Err(_) => { |
| 138 | 141 | tracing::warn!("Unauthenticated access attempt to: {} (no token)", uri); |
| 139 | - let redirect_url = | |
| 140 | - crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 142 | + let redirect_url = crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 141 | 143 | return Ok(Redirect::to(&redirect_url).into_response()); |
| 142 | 144 | } |
| 143 | 145 | }; |
@@ -154,8 +156,7 @@ pub async fn auth_required_impl(
| 154 | 156 | Ok(claims) => claims, |
| 155 | 157 | Err(e) => { |
| 156 | 158 | tracing::warn!("Invalid JWT token for {}: {}", uri, e); |
| 157 | - let redirect_url = | |
| 158 | - crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 159 | + let redirect_url = crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 159 | 160 | return Ok(Redirect::to(&redirect_url).into_response()); |
| 160 | 161 | } |
| 161 | 162 | }; |
M
crates/rustwarden-core/src/middleware/auth_service.rs
+4
-11
@@ -84,11 +84,7 @@ impl<S> AuthService<S> {
| 84 | 84 | /// such as `rustwarden-fediverse` register their own prefixes |
| 85 | 85 | /// (`/fediverse/consent`, `/fediverse/invite`) via the builder before |
| 86 | 86 | /// the app boots. |
| 87 | - pub fn new( | |
| 88 | - inner: S, | |
| 89 | - db: DatabaseConnection, | |
| 90 | - protected_prefixes: Arc<Vec<String>>, | |
| 91 | - ) -> Self { | |
| 87 | + pub fn new(inner: S, db: DatabaseConnection, protected_prefixes: Arc<Vec<String>>) -> Self { | |
| 92 | 88 | Self { |
| 93 | 89 | inner, |
| 94 | 90 | db, |
@@ -213,12 +209,9 @@ where
| 213 | 209 | // remote JWT issuance is also gated by that crate. |
| 214 | 210 | if claims.is_remote() { |
| 215 | 211 | if let Some(rid) = claims.remote_identity_id() { |
| 216 | - if let Some(checker) = parts | |
| 217 | - .extensions | |
| 218 | - .get::<std::sync::Arc< | |
| 219 | - dyn crate::middleware::revocation::RemoteIdentityRevocationChecker, | |
| 220 | - >>() | |
| 221 | - { | |
| 212 | + if let Some(checker) = parts.extensions.get::<std::sync::Arc< | |
| 213 | + dyn crate::middleware::revocation::RemoteIdentityRevocationChecker, | |
| 214 | + >>() { | |
| 222 | 215 | if checker.is_revoked(rid).await { |
| 223 | 216 | tracing::warn!( |
| 224 | 217 | "AuthService: remote_identity {} is revoked, rejecting JWT", |
M
crates/rustwarden-core/src/middleware/csrf.rs
+1
-1
@@ -20,7 +20,7 @@
| 20 | 20 | use axum::{ |
| 21 | 21 | body::Body, |
| 22 | 22 | extract::{Request, State}, |
| 23 | - http::{header::CONTENT_TYPE, Method, StatusCode, Uri}, | |
| 23 | + http::{Method, StatusCode, Uri, header::CONTENT_TYPE}, | |
| 24 | 24 | middleware::Next, |
| 25 | 25 | response::{IntoResponse, Response}, |
| 26 | 26 | }; |
M
crates/rustwarden-core/src/middleware/mod.rs
+3
-3
@@ -19,14 +19,14 @@ pub mod auth_service;
| 19 | 19 | /// [`crate::middleware::csrf::csrf_protection_with_config`], |
| 20 | 20 | /// and [`crate::middleware::csrf::CsrfProtectionConfig`]. |
| 21 | 21 | pub mod csrf; |
| 22 | -/// Trait consulted by `AuthService` to refuse revoked remote JWTs | |
| 23 | -/// (implemented and injected by `rustwarden-fediverse`). | |
| 24 | -pub mod revocation; | |
| 25 | 22 | /// Centralized error handling middleware that maps errors to structured responses. |
| 26 | 23 | pub mod error_handler; |
| 27 | 24 | /// Middleware that validates password complexity requirements. |
| 28 | 25 | pub mod password_complexity; |
| 29 | 26 | /// Token-bucket rate limiting middleware. |
| 30 | 27 | pub mod rate_limit; |
| 28 | +/// Trait consulted by `AuthService` to refuse revoked remote JWTs | |
| 29 | +/// (implemented and injected by `rustwarden-fediverse`). | |
| 30 | +pub mod revocation; | |
| 31 | 31 | /// Middleware that updates the user's last-login timestamp on successful requests. |
| 32 | 32 | pub mod update_last_login; |
M
crates/rustwarden-core/src/migrations/m20260510_000009_fediverse_interop.rs
+10
-2
@@ -51,7 +51,11 @@ impl MigrationTrait for Migration {
| 51 | 51 | .col(ColumnDef::new(RemoteIdentities::DisplayName).text().null()) |
| 52 | 52 | .col(ColumnDef::new(RemoteIdentities::SummaryHtml).text().null()) |
| 53 | 53 | .col(ColumnDef::new(RemoteIdentities::InboxUrl).text().not_null()) |
| 54 | - .col(ColumnDef::new(RemoteIdentities::SharedInboxUrl).text().null()) | |
| 54 | + .col( | |
| 55 | + ColumnDef::new(RemoteIdentities::SharedInboxUrl) | |
| 56 | + .text() | |
| 57 | + .null(), | |
| 58 | + ) | |
| 55 | 59 | .col( |
| 56 | 60 | ColumnDef::new(RemoteIdentities::PublicKeyPem) |
| 57 | 61 | .text() |
@@ -64,7 +68,11 @@ impl MigrationTrait for Migration {
| 64 | 68 | .not_null() |
| 65 | 69 | .default("Person"), |
| 66 | 70 | ) |
| 67 | - .col(ColumnDef::new(RemoteIdentities::RawJson).json_binary().null()) | |
| 71 | + .col( | |
| 72 | + ColumnDef::new(RemoteIdentities::RawJson) | |
| 73 | + .json_binary() | |
| 74 | + .null(), | |
| 75 | + ) | |
| 68 | 76 | .col( |
| 69 | 77 | ColumnDef::new(RemoteIdentities::LastFetchedAt) |
| 70 | 78 | .timestamp_with_time_zone() |
M
crates/rustwarden-core/src/services/app_settings_service.rs
+5
-11
@@ -322,7 +322,9 @@ impl AppSettingsService {
| 322 | 322 | ( |
| 323 | 323 | "oauth_redirect_base_url", |
| 324 | 324 | "", |
| 325 | - Some("Base URL for OAuth redirect callbacks (fallback to OAUTH_REDIRECT_BASE_URL env var)"), | |
| 325 | + Some( | |
| 326 | + "Base URL for OAuth redirect callbacks (fallback to OAUTH_REDIRECT_BASE_URL env var)", | |
| 327 | + ), | |
| 326 | 328 | ), |
| 327 | 329 | ( |
| 328 | 330 | "oauth_auto_register", |
@@ -334,21 +336,13 @@ impl AppSettingsService {
| 334 | 336 | "true", |
| 335 | 337 | Some("Auto-link OAuth account to existing user with same verified email"), |
| 336 | 338 | ), |
| 337 | - ( | |
| 338 | - "oauth_google_client_id", | |
| 339 | - "", | |
| 340 | - Some("Google OAuth Client ID"), | |
| 341 | - ), | |
| 339 | + ("oauth_google_client_id", "", Some("Google OAuth Client ID")), | |
| 342 | 340 | ( |
| 343 | 341 | "oauth_google_client_secret", |
| 344 | 342 | "", |
| 345 | 343 | Some("Google OAuth Client Secret (encrypted)"), |
| 346 | 344 | ), |
| 347 | - ( | |
| 348 | - "oauth_github_client_id", | |
| 349 | - "", | |
| 350 | - Some("GitHub OAuth Client ID"), | |
| 351 | - ), | |
| 345 | + ("oauth_github_client_id", "", Some("GitHub OAuth Client ID")), | |
| 352 | 346 | ( |
| 353 | 347 | "oauth_github_client_secret", |
| 354 | 348 | "", |
M
crates/rustwarden-core/src/services/crypto_service.rs
+2
-2
@@ -1,9 +1,9 @@
| 1 | 1 | use crate::error::AppError; |
| 2 | 2 | use aes_gcm::{ |
| 3 | - aead::{Aead, KeyInit, OsRng}, | |
| 4 | 3 | AeadCore, Aes256Gcm, Nonce, |
| 4 | + aead::{Aead, KeyInit, OsRng}, | |
| 5 | 5 | }; |
| 6 | -use base64::{engine::general_purpose::STANDARD as B64, Engine}; | |
| 6 | +use base64::{Engine, engine::general_purpose::STANDARD as B64}; | |
| 7 | 7 | use std::env; |
| 8 | 8 | |
| 9 | 9 | const ENCRYPTED_PREFIX: &str = "enc:"; |
M
crates/rustwarden-core/src/services/email_service.rs
+3
-3
@@ -6,13 +6,13 @@
| 6 | 6 | use crate::config::email::EmailConfig; |
| 7 | 7 | use crate::error::AppError; |
| 8 | 8 | use lettre::{ |
| 9 | - message::{header::ContentType, Mailbox, Message, MultiPart, SinglePart}, | |
| 9 | + Transport, | |
| 10 | + message::{Mailbox, Message, MultiPart, SinglePart, header::ContentType}, | |
| 10 | 11 | transport::smtp::{ |
| 12 | + SmtpTransport, | |
| 11 | 13 | authentication::Credentials, |
| 12 | 14 | client::{Certificate, Tls, TlsParameters}, |
| 13 | - SmtpTransport, | |
| 14 | 15 | }, |
| 15 | - Transport, | |
| 16 | 16 | }; |
| 17 | 17 | use std::sync::Arc; |
| 18 | 18 | use tokio::sync::Mutex; |
M
crates/rustwarden-core/src/services/jwt_blacklist_service.rs
+1
-1
@@ -229,7 +229,7 @@ impl JwtBlacklistService {
| 229 | 229 | /// |
| 230 | 230 | /// Returns `AppError::Internal` if JWT parsing fails or `exp` claim is missing |
| 231 | 231 | fn extract_expiration_from_jwt(jwt_token: &str) -> Result<chrono::DateTime<Utc>, AppError> { |
| 232 | - use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; | |
| 232 | + use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; | |
| 233 | 233 | use serde_json::Value; |
| 234 | 234 | |
| 235 | 235 | // JWT format: header.payload.signature |
M
crates/rustwarden-core/src/services/oauth/providers/google.rs
+2
-2
@@ -2,8 +2,8 @@ use super::super::provider_trait::{AuthorizationUrl, OAuthProvider, ProviderUser
| 2 | 2 | use crate::error::AppError; |
| 3 | 3 | use async_trait::async_trait; |
| 4 | 4 | use openidconnect::{ |
| 5 | - core::CoreProviderMetadata, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, | |
| 6 | - Nonce, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, | |
| 5 | + AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, PkceCodeChallenge, | |
| 6 | + PkceCodeVerifier, RedirectUrl, core::CoreProviderMetadata, | |
| 7 | 7 | }; |
| 8 | 8 | |
| 9 | 9 | /// OIDC provider implementation for Google. |
M
crates/rustwarden-core/src/services/oauth/providers/microsoft.rs
+2
-2
@@ -2,8 +2,8 @@ use super::super::provider_trait::{AuthorizationUrl, OAuthProvider, ProviderUser
| 2 | 2 | use crate::error::AppError; |
| 3 | 3 | use async_trait::async_trait; |
| 4 | 4 | use openidconnect::{ |
| 5 | - core::CoreProviderMetadata, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, | |
| 6 | - Nonce, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, | |
| 5 | + AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, PkceCodeChallenge, | |
| 6 | + PkceCodeVerifier, RedirectUrl, core::CoreProviderMetadata, | |
| 7 | 7 | }; |
| 8 | 8 | |
| 9 | 9 | /// OIDC provider implementation for Microsoft (Azure AD / Entra ID). |
M
crates/rustwarden-core/src/services/password_service.rs
+11
-7
@@ -1,7 +1,7 @@
| 1 | 1 | use crate::error::AppError; |
| 2 | 2 | use bcrypt::DEFAULT_COST; |
| 3 | -use rand::seq::SliceRandom; | |
| 4 | 3 | use rand::Rng; |
| 4 | +use rand::seq::SliceRandom; | |
| 5 | 5 | use std::sync::LazyLock; |
| 6 | 6 | |
| 7 | 7 | /// SEC-M5: Bcrypt cost factor, configurable via BCRYPT_COST env var (default: 12, min: 10, max: 20). |
@@ -210,14 +210,18 @@ mod tests {
| 210 | 210 | let hash = PasswordService::hash_password(password).await.unwrap(); |
| 211 | 211 | |
| 212 | 212 | // Verify correct password |
| 213 | - assert!(PasswordService::verify_password(password, &hash) | |
| 214 | - .await | |
| 215 | - .unwrap()); | |
| 213 | + assert!( | |
| 214 | + PasswordService::verify_password(password, &hash) | |
| 215 | + .await | |
| 216 | + .unwrap() | |
| 217 | + ); | |
| 216 | 218 | |
| 217 | 219 | // Verify incorrect password |
| 218 | - assert!(!PasswordService::verify_password("wrong_password", &hash) | |
| 219 | - .await | |
| 220 | - .unwrap()); | |
| 220 | + assert!( | |
| 221 | + !PasswordService::verify_password("wrong_password", &hash) | |
| 222 | + .await | |
| 223 | + .unwrap() | |
| 224 | + ); | |
| 221 | 225 | } |
| 222 | 226 | |
| 223 | 227 | #[test] |
M
crates/rustwarden-core/src/services/refresh_token_service.rs
+1
-1
@@ -10,7 +10,7 @@
| 10 | 10 | use crate::error::AppError; |
| 11 | 11 | use crate::models::refresh_token; |
| 12 | 12 | use chrono::{DateTime, Duration, Utc}; |
| 13 | -use rand::{distr::Alphanumeric, Rng}; | |
| 13 | +use rand::{Rng, distr::Alphanumeric}; | |
| 14 | 14 | use sea_orm::{ |
| 15 | 15 | ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, |
| 16 | 16 | QueryOrder, Set, |
M
crates/rustwarden-core/src/services/team_service.rs
+7
-7
@@ -215,9 +215,7 @@ impl TeamService {
| 215 | 215 | ) -> Result<Vec<team::Model>, AppError> { |
| 216 | 216 | use sea_orm::{JoinType, QuerySelect, RelationTrait}; |
| 217 | 217 | match identity { |
| 218 | - crate::identity::Identity::Local(user_id) => { | |
| 219 | - Self::teams_of_user(db, user_id).await | |
| 220 | - } | |
| 218 | + crate::identity::Identity::Local(user_id) => Self::teams_of_user(db, user_id).await, | |
| 221 | 219 | crate::identity::Identity::Remote(remote_id) => Ok(team::Entity::find() |
| 222 | 220 | .join_rev(JoinType::InnerJoin, team_member::Relation::Team.def()) |
| 223 | 221 | .filter(team_member::Column::RemoteIdentityId.eq(Some(remote_id))) |
@@ -540,10 +538,12 @@ mod tests {
| 540 | 538 | |
| 541 | 539 | TeamService::delete(&db, team.id, None).await.unwrap(); |
| 542 | 540 | |
| 543 | - assert!(TeamService::find_by_id(&db, team.id) | |
| 544 | - .await | |
| 545 | - .unwrap() | |
| 546 | - .is_none()); | |
| 541 | + assert!( | |
| 542 | + TeamService::find_by_id(&db, team.id) | |
| 543 | + .await | |
| 544 | + .unwrap() | |
| 545 | + .is_none() | |
| 546 | + ); | |
| 547 | 547 | let remaining = team_member::Entity::find() |
| 548 | 548 | .filter(team_member::Column::TeamId.eq(team.id)) |
| 549 | 549 | .all(&db) |
M
crates/rustwarden-core/tests/middleware_authconfig_fallback.rs
+7
-9
@@ -100,18 +100,16 @@ fn auth_config_from_env_yields_signature_consistent_loader() {
| 100 | 100 | let validator = AuthConfig::from_env().expect("validator load"); |
| 101 | 101 | |
| 102 | 102 | let user_id = Uuid::new_v4(); |
| 103 | - let token = JwtService::generate_token( | |
| 104 | - &signer, | |
| 105 | - user_id, | |
| 106 | - "fallback_user", | |
| 107 | - Vec::new(), | |
| 108 | - Vec::new(), | |
| 109 | - ) | |
| 110 | - .expect("token signing must succeed"); | |
| 103 | + let token = | |
| 104 | + JwtService::generate_token(&signer, user_id, "fallback_user", Vec::new(), Vec::new()) | |
| 105 | + .expect("token signing must succeed"); | |
| 111 | 106 | |
| 112 | 107 | let claims = JwtService::validate_token(&validator, &token) |
| 113 | 108 | .expect("token signed by from_env() must validate against another from_env()"); |
| 114 | 109 | |
| 115 | - assert_eq!(claims.sub, user_id, "validated claims must round-trip the user id"); | |
| 110 | + assert_eq!( | |
| 111 | + claims.sub, user_id, | |
| 112 | + "validated claims must round-trip the user id" | |
| 113 | + ); | |
| 116 | 114 | assert_eq!(claims.username, "fallback_user"); |
| 117 | 115 | } |
M
crates/rustwarden-fediverse-guard/migration/src/lib.rs
+1
-3
@@ -32,8 +32,6 @@ impl MigratorTrait for FediverseGuardMigrator {
| 32 | 32 | } |
| 33 | 33 | |
| 34 | 34 | fn migrations() -> Vec<Box<dyn MigrationTrait>> { |
| 35 | - vec![Box::new( | |
| 36 | - m20260514_000001_fediverse_guard_schema::Migration, | |
| 37 | - )] | |
| 35 | + vec![Box::new(m20260514_000001_fediverse_guard_schema::Migration)] | |
| 38 | 36 | } |
| 39 | 37 | } |
M
crates/rustwarden-fediverse-guard/migration/src/m20260514_000001_fediverse_guard_schema.rs
+5
-25
@@ -96,17 +96,9 @@ impl MigrationTrait for Migration {
| 96 | 96 | .not_null() |
| 97 | 97 | .primary_key(), |
| 98 | 98 | ) |
| 99 | - .col( | |
| 100 | - ColumnDef::new(FediverseGuardBans::Domain) | |
| 101 | - .text() | |
| 102 | - .not_null(), | |
| 103 | - ) | |
| 99 | + .col(ColumnDef::new(FediverseGuardBans::Domain).text().not_null()) | |
| 104 | 100 | .col(ColumnDef::new(FediverseGuardBans::ActorUri).text().null()) |
| 105 | - .col( | |
| 106 | - ColumnDef::new(FediverseGuardBans::Reason) | |
| 107 | - .text() | |
| 108 | - .not_null(), | |
| 109 | - ) | |
| 101 | + .col(ColumnDef::new(FediverseGuardBans::Reason).text().not_null()) | |
| 110 | 102 | .col( |
| 111 | 103 | ColumnDef::new(FediverseGuardBans::Severity) |
| 112 | 104 | .text() |
@@ -132,11 +124,7 @@ impl MigrationTrait for Migration {
| 132 | 124 | .timestamp_with_time_zone() |
| 133 | 125 | .null(), |
| 134 | 126 | ) |
| 135 | - .col( | |
| 136 | - ColumnDef::new(FediverseGuardBans::LiftedBy) | |
| 137 | - .uuid() | |
| 138 | - .null(), | |
| 139 | - ) | |
| 127 | + .col(ColumnDef::new(FediverseGuardBans::LiftedBy).uuid().null()) | |
| 140 | 128 | .to_owned(), |
| 141 | 129 | ) |
| 142 | 130 | .await?; |
@@ -158,16 +146,8 @@ impl MigrationTrait for Migration {
| 158 | 146 | .not_null() |
| 159 | 147 | .primary_key(), |
| 160 | 148 | ) |
| 161 | - .col( | |
| 162 | - ColumnDef::new(FediverseGuardAcl::Kind) | |
| 163 | - .text() | |
| 164 | - .not_null(), | |
| 165 | - ) | |
| 166 | - .col( | |
| 167 | - ColumnDef::new(FediverseGuardAcl::Domain) | |
| 168 | - .text() | |
| 169 | - .not_null(), | |
| 170 | - ) | |
| 149 | + .col(ColumnDef::new(FediverseGuardAcl::Kind).text().not_null()) | |
| 150 | + .col(ColumnDef::new(FediverseGuardAcl::Domain).text().not_null()) | |
| 171 | 151 | .col(ColumnDef::new(FediverseGuardAcl::Reason).text().null()) |
| 172 | 152 | .col( |
| 173 | 153 | ColumnDef::new(FediverseGuardAcl::CreatedBy) |
M
crates/rustwarden-fediverse-guard/src/ban.rs
+6
-1
@@ -118,7 +118,12 @@ impl BanManager {
| 118 | 118 | } |
| 119 | 119 | } |
| 120 | 120 | let now = Utc::now(); |
| 121 | - if !self.store.active_bans_for_domain(domain, now).await?.is_empty() { | |
| 121 | + if !self | |
| 122 | + .store | |
| 123 | + .active_bans_for_domain(domain, now) | |
| 124 | + .await? | |
| 125 | + .is_empty() | |
| 126 | + { | |
| 122 | 127 | return Ok(None); |
| 123 | 128 | } |
| 124 | 129 | let expires_at = self.config.auto_ban_duration.map(|d| { |
M
crates/rustwarden-fediverse-guard/src/config.rs
+17
-9
@@ -125,7 +125,11 @@ impl GuardConfig {
| 125 | 125 | }, |
| 126 | 126 | auto_ban_duration: { |
| 127 | 127 | let secs = parse_u64_env("FEDIVERSE_GUARD_AUTO_BAN_DURATION_SECS", 3600)?; |
| 128 | - if secs == 0 { None } else { Some(Duration::from_secs(secs)) } | |
| 128 | + if secs == 0 { | |
| 129 | + None | |
| 130 | + } else { | |
| 131 | + Some(Duration::from_secs(secs)) | |
| 132 | + } | |
| 129 | 133 | }, |
| 130 | 134 | events_retention: Duration::from_secs( |
| 131 | 135 | parse_u64_env("FEDIVERSE_GUARD_EVENTS_RETENTION_DAYS", 90)? * 24 * 3600, |
@@ -166,20 +170,24 @@ fn parse_bool_env(name: &'static str, default: bool) -> Result<bool, GuardConfig
| 166 | 170 | |
| 167 | 171 | fn parse_u32_env(name: &'static str, default: u32) -> Result<u32, GuardConfigError> { |
| 168 | 172 | match std::env::var(name) { |
| 169 | - Ok(v) => v.parse::<u32>().map_err(|e| GuardConfigError::InvalidValue { | |
| 170 | - name, | |
| 171 | - source: Box::new(e), | |
| 172 | - }), | |
| 173 | + Ok(v) => v | |
| 174 | + .parse::<u32>() | |
| 175 | + .map_err(|e| GuardConfigError::InvalidValue { | |
| 176 | + name, | |
| 177 | + source: Box::new(e), | |
| 178 | + }), | |
| 173 | 179 | Err(_) => Ok(default), |
| 174 | 180 | } |
| 175 | 181 | } |
| 176 | 182 | |
| 177 | 183 | fn parse_u64_env(name: &'static str, default: u64) -> Result<u64, GuardConfigError> { |
| 178 | 184 | match std::env::var(name) { |
| 179 | - Ok(v) => v.parse::<u64>().map_err(|e| GuardConfigError::InvalidValue { | |
| 180 | - name, | |
| 181 | - source: Box::new(e), | |
| 182 | - }), | |
| 185 | + Ok(v) => v | |
| 186 | + .parse::<u64>() | |
| 187 | + .map_err(|e| GuardConfigError::InvalidValue { | |
| 188 | + name, | |
| 189 | + source: Box::new(e), | |
| 190 | + }), | |
| 183 | 191 | Err(_) => Ok(default), |
| 184 | 192 | } |
| 185 | 193 | } |
M
crates/rustwarden-fediverse-guard/src/detector/mod.rs
+14
-7
@@ -68,7 +68,11 @@ impl ThresholdDetector {
| 68 | 68 | kind: GuardEventKind, |
| 69 | 69 | threshold: DetectorThreshold, |
| 70 | 70 | ) -> Self { |
| 71 | - Self { name, kind, threshold } | |
| 71 | + Self { | |
| 72 | + name, | |
| 73 | + kind, | |
| 74 | + threshold, | |
| 75 | + } | |
| 72 | 76 | } |
| 73 | 77 | |
| 74 | 78 | /// Returns the threshold (count + window) carried by this detector. |
@@ -256,7 +260,13 @@ mod tests {
| 256 | 260 | .await |
| 257 | 261 | .unwrap(); |
| 258 | 262 | let det = signature_detector(); |
| 259 | - push_events(m.store(), GuardEventKind::SignatureFailure, "trusted.tld", 10).await; | |
| 263 | + push_events( | |
| 264 | + m.store(), | |
| 265 | + GuardEventKind::SignatureFailure, | |
| 266 | + "trusted.tld", | |
| 267 | + 10, | |
| 268 | + ) | |
| 269 | + .await; | |
| 260 | 270 | assert!(!det.check(&m, "trusted.tld").await.unwrap()); |
| 261 | 271 | } |
| 262 | 272 |
@@ -288,11 +298,8 @@ mod tests {
| 288 | 298 | let store = m.store().clone(); |
| 289 | 299 | let old = Utc::now() - chrono::Duration::minutes(10); |
| 290 | 300 | for _ in 0..4 { |
| 291 | - let mut evt = GuardEvent::new( | |
| 292 | - GuardEventKind::SignatureFailure, | |
| 293 | - "x.tld", | |
| 294 | - Severity::Medium, | |
| 295 | - ); | |
| 301 | + let mut evt = | |
| 302 | + GuardEvent::new(GuardEventKind::SignatureFailure, "x.tld", Severity::Medium); | |
| 296 | 303 | evt.created_at = old; |
| 297 | 304 | store.insert_event(&evt).await.unwrap(); |
| 298 | 305 | } |
M
crates/rustwarden-fediverse-guard/src/lib.rs
+1
-1
@@ -30,7 +30,7 @@ pub mod ui;
| 30 | 30 | |
| 31 | 31 | pub use ban::{BanDecision, BanManager}; |
| 32 | 32 | pub use config::{DetectorThreshold, GuardConfig, GuardConfigError}; |
| 33 | -pub use detector::{default_detectors, run_all, Detector, ThresholdDetector}; | |
| 33 | +pub use detector::{Detector, ThresholdDetector, default_detectors, run_all}; | |
| 34 | 34 | pub use events::{GuardEvent, GuardEventKind, GuardEventSink}; |
| 35 | 35 | pub use observer::GuardObserver; |
| 36 | 36 | pub use store::{AclEntry, AclKind, BanEntry, GuardStore, HybridStore, MemoryStore, PostgresStore}; |
M
crates/rustwarden-fediverse-guard/src/observer.rs
+9
-20
@@ -21,7 +21,7 @@ use rustwarden_fediverse::observer::FediverseEventObserver;
| 21 | 21 | use serde_json::Value; |
| 22 | 22 | |
| 23 | 23 | use crate::ban::BanManager; |
| 24 | -use crate::detector::{run_all, Detector}; | |
| 24 | +use crate::detector::{Detector, run_all}; | |
| 25 | 25 | use crate::events::{GuardEvent, GuardEventKind, GuardEventSink, Severity}; |
| 26 | 26 | use crate::store::normalise_domain; |
| 27 | 27 |
@@ -42,7 +42,11 @@ impl GuardObserver {
| 42 | 42 | detectors: Vec<Arc<dyn Detector>>, |
| 43 | 43 | sink: Option<Arc<dyn GuardEventSink>>, |
| 44 | 44 | ) -> Self { |
| 45 | - Self { manager, detectors, sink } | |
| 45 | + Self { | |
| 46 | + manager, | |
| 47 | + detectors, | |
| 48 | + sink, | |
| 49 | + } | |
| 46 | 50 | } |
| 47 | 51 | |
| 48 | 52 | /// Borrow the underlying [`BanManager`] (useful for admin |
@@ -85,12 +89,7 @@ impl GuardObserver {
| 85 | 89 | |
| 86 | 90 | #[async_trait] |
| 87 | 91 | impl FediverseEventObserver for GuardObserver { |
| 88 | - async fn on_signature_failure( | |
| 89 | - &self, | |
| 90 | - domain: &str, | |
| 91 | - actor_uri: Option<&str>, | |
| 92 | - reason: &str, | |
| 93 | - ) { | |
| 92 | + async fn on_signature_failure(&self, domain: &str, actor_uri: Option<&str>, reason: &str) { | |
| 94 | 93 | self.record( |
| 95 | 94 | GuardEventKind::SignatureFailure, |
| 96 | 95 | domain, |
@@ -101,12 +100,7 @@ impl FediverseEventObserver for GuardObserver {
| 101 | 100 | .await; |
| 102 | 101 | } |
| 103 | 102 | |
| 104 | - async fn on_replay_attempt( | |
| 105 | - &self, | |
| 106 | - domain: &str, | |
| 107 | - actor_uri: Option<&str>, | |
| 108 | - details: Value, | |
| 109 | - ) { | |
| 103 | + async fn on_replay_attempt(&self, domain: &str, actor_uri: Option<&str>, details: Value) { | |
| 110 | 104 | self.record( |
| 111 | 105 | GuardEventKind::ReplayAttempt, |
| 112 | 106 | domain, |
@@ -129,12 +123,7 @@ impl FediverseEventObserver for GuardObserver {
| 129 | 123 | .await; |
| 130 | 124 | } |
| 131 | 125 | |
| 132 | - async fn on_inbox_abuse( | |
| 133 | - &self, | |
| 134 | - domain: &str, | |
| 135 | - actor_uri: Option<&str>, | |
| 136 | - reason: &str, | |
| 137 | - ) { | |
| 126 | + async fn on_inbox_abuse(&self, domain: &str, actor_uri: Option<&str>, reason: &str) { | |
| 138 | 127 | self.record( |
| 139 | 128 | GuardEventKind::InboxAbuse, |
| 140 | 129 | domain, |
M
crates/rustwarden-fediverse-guard/src/store/hybrid.rs
+4
-1
@@ -227,7 +227,10 @@ mod tests {
| 227 | 227 | assert_eq!(acl_n, 1); |
| 228 | 228 | |
| 229 | 229 | // Cache should answer the hot path without hitting durable. |
| 230 | - let active_cached = hybrid.active_bans_for_domain("evil.tld", now).await.unwrap(); | |
| 230 | + let active_cached = hybrid | |
| 231 | + .active_bans_for_domain("evil.tld", now) | |
| 232 | + .await | |
| 233 | + .unwrap(); | |
| 231 | 234 | assert_eq!(active_cached.len(), 1); |
| 232 | 235 | let acl_cached = hybrid.acl_for_domain("peer.tld").await.unwrap(); |
| 233 | 236 | assert!(acl_cached.is_some()); |
M
crates/rustwarden-fediverse-guard/src/store/memory.rs
+4
-1
@@ -170,7 +170,10 @@ mod tests {
| 170 | 170 | .unwrap(); |
| 171 | 171 | assert_eq!(active.len(), 1); |
| 172 | 172 | let admin = Uuid::new_v4(); |
| 173 | - store.lift_ban(ban_id, Some(admin), Utc::now()).await.unwrap(); | |
| 173 | + store | |
| 174 | + .lift_ban(ban_id, Some(admin), Utc::now()) | |
| 175 | + .await | |
| 176 | + .unwrap(); | |
| 174 | 177 | let active_after = store |
| 175 | 178 | .active_bans_for_domain("evil.tld", now + chrono::Duration::minutes(10)) |
| 176 | 179 | .await |
M
crates/rustwarden-fediverse-guard/src/store/postgres.rs
+4
-1
@@ -245,7 +245,10 @@ mod tests {
| 245 | 245 | |
| 246 | 246 | #[test] |
| 247 | 247 | fn serialize_kind_is_stable_snake_case() { |
| 248 | - assert_eq!(serialize_kind(GuardEventKind::SignatureFailure), "signature_failure"); | |
| 248 | + assert_eq!( | |
| 249 | + serialize_kind(GuardEventKind::SignatureFailure), | |
| 250 | + "signature_failure" | |
| 251 | + ); | |
| 249 | 252 | assert_eq!(serialize_kind(GuardEventKind::FetchSsrf), "fetch_ssrf"); |
| 250 | 253 | assert_eq!(serialize_kind(GuardEventKind::Unbanned), "unbanned"); |
| 251 | 254 | } |
M
crates/rustwarden-fediverse-guard/src/ui/handlers.rs
+5
-2
@@ -185,7 +185,11 @@ fn event_row_vm(m: &event::Model) -> EventRowVM {
| 185 | 185 | } |
| 186 | 186 | } |
| 187 | 187 | |
| 188 | -fn threshold_row(name: &str, t: DetectorThreshold, ttl: Option<std::time::Duration>) -> ThresholdRowVM { | |
| 188 | +fn threshold_row( | |
| 189 | + name: &str, | |
| 190 | + t: DetectorThreshold, | |
| 191 | + ttl: Option<std::time::Duration>, | |
| 192 | +) -> ThresholdRowVM { | |
| 189 | 193 | ThresholdRowVM { |
| 190 | 194 | name: name.to_string(), |
| 191 | 195 | count: t.count, |
@@ -405,4 +409,3 @@ pub async fn thresholds(
| 405 | 409 | thresholds: rows, |
| 406 | 410 | }) |
| 407 | 411 | } |
| 408 | - |
M
crates/rustwarden-fediverse-guard/src/ui/routes.rs
+1
-1
@@ -6,8 +6,8 @@
| 6 | 6 | //! `/admin/*` route — including the five mounted here — so no extra |
| 7 | 7 | //! authorization layering is required. |
| 8 | 8 | |
| 9 | -use axum::routing::get; | |
| 10 | 9 | use axum::Router; |
| 10 | +use axum::routing::get; | |
| 11 | 11 | use sea_orm::DatabaseConnection; |
| 12 | 12 | |
| 13 | 13 | use super::handlers; |
M
crates/rustwarden-fediverse-guard/tests/integration_pipeline.rs
+3
-9
@@ -15,8 +15,7 @@ use std::sync::Arc;
| 15 | 15 | use chrono::Utc; |
| 16 | 16 | use rustwarden_fediverse::observer::FediverseEventObserver; |
| 17 | 17 | use rustwarden_fediverse_guard::{ |
| 18 | - default_detectors, BanDecision, BanManager, GuardConfig, GuardObserver, GuardStore, | |
| 19 | - MemoryStore, | |
| 18 | + BanDecision, BanManager, GuardConfig, GuardObserver, GuardStore, MemoryStore, default_detectors, | |
| 20 | 19 | }; |
| 21 | 20 | |
| 22 | 21 | fn fresh_observer(cfg: GuardConfig) -> (GuardObserver, BanManager) { |
@@ -88,9 +87,7 @@ async fn unrelated_domains_are_not_collateral_banned() {
| 88 | 87 | let (observer, manager) = fresh_observer(cfg); |
| 89 | 88 | |
| 90 | 89 | for _ in 0..10 { |
| 91 | - observer | |
| 92 | - .on_signature_failure("bad.tld", None, "x") | |
| 93 | - .await; | |
| 90 | + observer.on_signature_failure("bad.tld", None, "x").await; | |
| 94 | 91 | } |
| 95 | 92 | |
| 96 | 93 | assert!(manager.is_banned("bad.tld").await.expect("is_banned")); |
@@ -167,10 +164,7 @@ async fn allow_acl_bypasses_active_ban() {
| 167 | 164 | }; |
| 168 | 165 | store.upsert_acl(&acl).await.expect("upsert_acl"); |
| 169 | 166 | let decision = manager.decide("partner.tld").await.expect("decide"); |
| 170 | - assert!( | |
| 171 | - matches!(decision, BanDecision::Allow), | |
| 172 | - "got {decision:?}" | |
| 173 | - ); | |
| 167 | + assert!(matches!(decision, BanDecision::Allow), "got {decision:?}"); | |
| 174 | 168 | } |
| 175 | 169 | |
| 176 | 170 | #[tokio::test] |
M
crates/rustwarden-fediverse-ui/src/dto/mod.rs
+1
-1
@@ -3,4 +3,4 @@
| 3 | 3 | /// Tab navigation entries for the `/admin/fediverse/*` screens. |
| 4 | 4 | mod admin_tabs; |
| 5 | 5 | |
| 6 | -pub use admin_tabs::{admin_tabs, AdminTabDTO}; | |
| 6 | +pub use admin_tabs::{AdminTabDTO, admin_tabs}; |
M
crates/rustwarden-fediverse-ui/src/handlers/admin.rs
+18
-52
@@ -33,22 +33,22 @@ use rustwarden_fediverse::services::fediverse_trust_service::{
| 33 | 33 | BlockSeverity, FediverseTrustService, TrustMode, |
| 34 | 34 | }; |
| 35 | 35 | |
| 36 | -use rustwarden_fediverse::models::fediverse_outbound_queue; | |
| 37 | -use rustwarden_fediverse::models::remote_identity; | |
| 36 | +use rustwarden_core::models::team; | |
| 38 | 37 | use rustwarden_core::models::user as user_model; |
| 38 | +use rustwarden_core::services::team_service::TeamRole; | |
| 39 | +use rustwarden_fediverse::models::fediverse_outbound_queue; | |
| 39 | 40 | use rustwarden_fediverse::models::fediverse_pending_invite; |
| 40 | -use rustwarden_core::models::team; | |
| 41 | +use rustwarden_fediverse::models::remote_identity; | |
| 41 | 42 | use rustwarden_fediverse::services::fediverse_invite_service::{ |
| 42 | 43 | FediverseInviteService, InviteStatus, |
| 43 | 44 | }; |
| 44 | -use rustwarden_core::services::team_service::TeamRole; | |
| 45 | 45 | use rustwarden_fediverse::services::fediverse_outbound_service::OutboundStatus; |
| 46 | 46 | use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect}; |
| 47 | 47 | |
| 48 | 48 | use rustwarden_ui::dto::NavigationDTO; |
| 49 | 49 | use rustwarden_ui::template_helpers::build_base_context_with_claims; |
| 50 | 50 | |
| 51 | -use crate::dto::{admin_tabs, AdminTabDTO}; | |
| 51 | +use crate::dto::{AdminTabDTO, admin_tabs}; | |
| 52 | 52 | |
| 53 | 53 | /// Navbar entry consumed by `components/navigation.html` (admin pages). |
| 54 | 54 | /// |
@@ -236,11 +236,7 @@ rustwarden_core::impl_template_response!(FediverseSettingsTemplate);
| 236 | 236 | /// Allowed values for `fediverse_trust_mode` (RFC 0004). |
| 237 | 237 | const TRUST_MODES: &[&str] = &["allowlist", "open_with_blocklist"]; |
| 238 | 238 | /// Allowed values for `fediverse_interop_scope` (master plan §2.1). |
| 239 | -const INTEROP_SCOPES: &[&str] = &[ | |
| 240 | - "rustwarden_only", | |
| 241 | - "rustwarden_plus_universal_read", | |
| 242 | - "open", | |
| 243 | -]; | |
| 239 | +const INTEROP_SCOPES: &[&str] = &["rustwarden_only", "rustwarden_plus_universal_read", "open"]; | |
| 244 | 240 | |
| 245 | 241 | /// Render `GET /admin/fediverse/settings`. Pre-fills the form with the |
| 246 | 242 | /// current `app_settings` rows and shows a one-shot toast when the |
@@ -311,10 +307,8 @@ pub async fn get_settings(
| 311 | 307 | trust_mode_allowlist: t("fediverse.settings.trust_mode_allowlist").await, |
| 312 | 308 | trust_mode_open: t("fediverse.settings.trust_mode_open").await, |
| 313 | 309 | interop_scope_label: t("fediverse.settings.interop_scope_label").await, |
| 314 | - interop_scope_rustwarden_only: t("fediverse.settings.interop_scope_rustwarden_only") | |
| 315 | - .await, | |
| 316 | - interop_scope_rustwarden_plus: t("fediverse.settings.interop_scope_rustwarden_plus") | |
| 317 | - .await, | |
| 310 | + interop_scope_rustwarden_only: t("fediverse.settings.interop_scope_rustwarden_only").await, | |
| 311 | + interop_scope_rustwarden_plus: t("fediverse.settings.interop_scope_rustwarden_plus").await, | |
| 318 | 312 | interop_scope_open: t("fediverse.settings.interop_scope_open").await, |
| 319 | 313 | authorized_fetch_label: t("fediverse.settings.authorized_fetch_label").await, |
| 320 | 314 | authorized_fetch_desc: t("fediverse.settings.authorized_fetch_desc").await, |
@@ -545,11 +539,7 @@ pub async fn get_trust_list(
| 545 | 539 | let trust = FediverseTrustService::new(db.clone()); |
| 546 | 540 | |
| 547 | 541 | if !trust.is_enabled().await? { |
| 548 | - return Ok(( | |
| 549 | - axum::http::StatusCode::NOT_FOUND, | |
| 550 | - "Federation disabled", | |
| 551 | - ) | |
| 552 | - .into_response()); | |
| 542 | + return Ok((axum::http::StatusCode::NOT_FOUND, "Federation disabled").into_response()); | |
| 553 | 543 | } |
| 554 | 544 | |
| 555 | 545 | let locale_str = locale.as_str(); |
@@ -688,9 +678,7 @@ pub async fn add_trust_entry(
| 688 | 678 | |
| 689 | 679 | match mode { |
| 690 | 680 | TrustMode::Allowlist => { |
| 691 | - trust | |
| 692 | - .add_trusted_domain(domain, note_opt, added_by) | |
| 693 | - .await?; | |
| 681 | + trust.add_trusted_domain(domain, note_opt, added_by).await?; | |
| 694 | 682 | } |
| 695 | 683 | TrustMode::OpenWithBlocklist => { |
| 696 | 684 | if !SEVERITIES.contains(&form.severity.as_str()) { |
@@ -915,14 +903,8 @@ pub async fn get_queue(
| 915 | 903 | attempts: m.attempts, |
| 916 | 904 | max_attempts: m.max_attempts, |
| 917 | 905 | has_last_error: m.last_error.as_deref().is_some_and(|s| !s.is_empty()), |
| 918 | - last_error: m | |
| 919 | - .last_error | |
| 920 | - .map(|e| truncate(&e, 80)) | |
| 921 | - .unwrap_or_default(), | |
| 922 | - next_retry_at: m | |
| 923 | - .next_retry_at | |
| 924 | - .map(|t| t.to_rfc3339()) | |
| 925 | - .unwrap_or_default(), | |
| 906 | + last_error: m.last_error.map(|e| truncate(&e, 80)).unwrap_or_default(), | |
| 907 | + next_retry_at: m.next_retry_at.map(|t| t.to_rfc3339()).unwrap_or_default(), | |
| 926 | 908 | created_at: m.created_at.format("%Y-%m-%d %H:%M:%S").to_string(), |
| 927 | 909 | }) |
| 928 | 910 | .collect(); |
@@ -1117,10 +1099,7 @@ pub async fn get_sessions_remote(
| 1117 | 1099 | id: m.id.to_string(), |
| 1118 | 1100 | ap_id: truncate(&m.ap_id, 60), |
| 1119 | 1101 | handle: format!("@{}@{}", m.username, m.domain), |
| 1120 | - has_display_name: m | |
| 1121 | - .display_name | |
| 1122 | - .as_deref() | |
| 1123 | - .is_some_and(|s| !s.is_empty()), | |
| 1102 | + has_display_name: m.display_name.as_deref().is_some_and(|s| !s.is_empty()), | |
| 1124 | 1103 | display_name: m.display_name.clone().unwrap_or_default(), |
| 1125 | 1104 | domain: m.domain.clone(), |
| 1126 | 1105 | last_seen: m.last_fetched_at.format("%Y-%m-%d %H:%M").to_string(), |
@@ -1376,8 +1355,7 @@ pub async fn get_nodeinfo(
| 1376 | 1355 | .map(|s| s.value) |
| 1377 | 1356 | .unwrap_or_else(|| "rustwarden_only".to_string()); |
| 1378 | 1357 | let authorized_fetch = |
| 1379 | - AppSettingsService::get_bool(&db, "fediverse_authorized_fetch", false) | |
| 1380 | - .await?; | |
| 1358 | + AppSettingsService::get_bool(&db, "fediverse_authorized_fetch", false).await?; | |
| 1381 | 1359 | |
| 1382 | 1360 | // User counts (local users). |
| 1383 | 1361 | let users_total = user_model::Entity::find().count(&db).await.unwrap_or(0); |
@@ -1718,17 +1696,13 @@ pub async fn create_team_invite(
| 1718 | 1696 | "handle must be of form @user@domain".into(), |
| 1719 | 1697 | )); |
| 1720 | 1698 | } |
| 1721 | - let role = TeamRole::parse(&form.role).ok_or_else(|| { | |
| 1722 | - AppError::Validation(format!("invalid role: {}", form.role)) | |
| 1723 | - })?; | |
| 1699 | + let role = TeamRole::parse(&form.role) | |
| 1700 | + .ok_or_else(|| AppError::Validation(format!("invalid role: {}", form.role)))?; | |
| 1724 | 1701 | |
| 1725 | 1702 | FediverseInviteService::invite_remote(&db, team_id, handle, role, claims.sub, INVITE_TTL) |
| 1726 | 1703 | .await?; |
| 1727 | 1704 | |
| 1728 | - Ok(Redirect::to(&format!( | |
| 1729 | - "/admin/fediverse/invites/{team_id}?saved=1" | |
| 1730 | - )) | |
| 1731 | - .into_response()) | |
| 1705 | + Ok(Redirect::to(&format!("/admin/fediverse/invites/{team_id}?saved=1")).into_response()) | |
| 1732 | 1706 | } |
| 1733 | 1707 | |
| 1734 | 1708 | /// `POST /admin/fediverse/invites/{team_id}/revoke` — revoke an invite. |
@@ -1762,10 +1736,7 @@ pub async fn revoke_team_invite(
| 1762 | 1736 | |
| 1763 | 1737 | FediverseInviteService::revoke(&db, invite_id).await?; |
| 1764 | 1738 | |
| 1765 | - Ok(Redirect::to(&format!( | |
| 1766 | - "/admin/fediverse/invites/{team_id}?saved=1" | |
| 1767 | - )) | |
| 1768 | - .into_response()) | |
| 1739 | + Ok(Redirect::to(&format!("/admin/fediverse/invites/{team_id}?saved=1")).into_response()) | |
| 1769 | 1740 | } |
| 1770 | 1741 | |
| 1771 | 1742 | // ───────────────────────────────────────────────────────────────────── |
@@ -2094,11 +2065,6 @@ mod tests {
| 2094 | 2065 | } |
| 2095 | 2066 | |
| 2096 | 2067 | #[test] |
| 2097 | - fn interop_scopes_is_non_empty() { | |
| 2098 | - assert!(!INTEROP_SCOPES.is_empty()); | |
| 2099 | - } | |
| 2100 | - | |
| 2101 | - #[test] | |
| 2102 | 2068 | fn severities_covers_three_documented_levels() { |
| 2103 | 2069 | assert!(SEVERITIES.contains(&"suspend")); |
| 2104 | 2070 | assert!(SEVERITIES.contains(&"silence")); |
M
crates/rustwarden-fediverse-ui/src/handlers/user_auth.rs
+22
-29
@@ -17,28 +17,28 @@ use axum::extract::{Extension, Form, Path, Query, State};
| 17 | 17 | use axum::response::sse::{Event, KeepAlive, Sse}; |
| 18 | 18 | use axum::response::{IntoResponse, Redirect}; |
| 19 | 19 | use futures::stream::Stream; |
| 20 | -use uuid::Uuid; | |
| 21 | 20 | use rustwarden_core::middleware::csrf; |
| 22 | 21 | use sea_orm::DatabaseConnection; |
| 23 | 22 | use serde::Deserialize; |
| 24 | 23 | use tower_sessions::Session; |
| 24 | +use uuid::Uuid; | |
| 25 | 25 | |
| 26 | +use rustwarden_core::auth::jwt::Claims; | |
| 26 | 27 | use rustwarden_core::error::AppError; |
| 27 | 28 | use rustwarden_core::i18n::extractor::Locale; |
| 28 | 29 | use rustwarden_core::i18n::service::I18nService; |
| 29 | -use rustwarden_core::auth::jwt::Claims; | |
| 30 | +use rustwarden_fediverse::FediverseConfig; | |
| 31 | +use rustwarden_fediverse::models::fediverse_pending_invite; | |
| 32 | +use rustwarden_fediverse::models::remote_identity; | |
| 30 | 33 | use rustwarden_fediverse::services::fediverse_auth_service::{ |
| 31 | - build_auth_decision_payload, ChallengeDecision, ChallengeStatus, FediverseAuthService, | |
| 32 | - InitiateRequest, | |
| 34 | + ChallengeDecision, ChallengeStatus, FediverseAuthService, InitiateRequest, | |
| 35 | + build_auth_decision_payload, | |
| 33 | 36 | }; |
| 34 | 37 | use rustwarden_fediverse::services::fediverse_invite_service::FediverseInviteService; |
| 35 | -use rustwarden_fediverse::models::fediverse_pending_invite; | |
| 36 | -use rustwarden_fediverse::services::fediverse_trust_service::FediverseTrustService; | |
| 37 | 38 | use rustwarden_fediverse::services::fediverse_outbound_service::{ |
| 38 | 39 | EnqueueRequest, FediverseOutboundService, |
| 39 | 40 | }; |
| 40 | -use rustwarden_fediverse::FediverseConfig; | |
| 41 | -use rustwarden_fediverse::models::remote_identity; | |
| 41 | +use rustwarden_fediverse::services::fediverse_trust_service::FediverseTrustService; | |
| 42 | 42 | |
| 43 | 43 | use rustwarden_fediverse::models::fediverse_auth_challenge; |
| 44 | 44 | use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; |
@@ -140,11 +140,7 @@ pub async fn error(
| 140 | 140 | ) -> Result<axum::response::Response, AppError> { |
| 141 | 141 | let trust = FediverseTrustService::new(db.clone()); |
| 142 | 142 | if !trust.is_enabled().await? { |
| 143 | - return Ok(( | |
| 144 | - axum::http::StatusCode::NOT_FOUND, | |
| 145 | - "Federation disabled", | |
| 146 | - ) | |
| 147 | - .into_response()); | |
| 143 | + return Ok((axum::http::StatusCode::NOT_FOUND, "Federation disabled").into_response()); | |
| 148 | 144 | } |
| 149 | 145 | |
| 150 | 146 | let reason = ErrorReason::parse(query.reason.as_deref().unwrap_or("")); |
@@ -302,11 +298,9 @@ pub async fn login_initiate(
| 302 | 298 | redirect_url: None, |
| 303 | 299 | }; |
| 304 | 300 | match FediverseAuthService::initiate(&db, req, CHALLENGE_TTL).await { |
| 305 | - Ok(challenge) => Ok(Redirect::to(&format!( | |
| 306 | - "/login/fediverse/waiting/{}", | |
| 307 | - challenge.id | |
| 308 | - )) | |
| 309 | - .into_response()), | |
| 301 | + Ok(challenge) => { | |
| 302 | + Ok(Redirect::to(&format!("/login/fediverse/waiting/{}", challenge.id)).into_response()) | |
| 303 | + } | |
| 310 | 304 | Err(AppError::Validation(_)) => { |
| 311 | 305 | Ok(Redirect::to("/login/fediverse/error?reason=unknown").into_response()) |
| 312 | 306 | } |
@@ -354,7 +348,10 @@ pub async fn waiting(
| 354 | 348 | |
| 355 | 349 | // Verify the challenge exists; otherwise show the error page rather |
| 356 | 350 | // than a stale spinner. |
| 357 | - if FediverseAuthService::status(&db, challenge_id).await.is_err() { | |
| 351 | + if FediverseAuthService::status(&db, challenge_id) | |
| 352 | + .await | |
| 353 | + .is_err() | |
| 354 | + { | |
| 358 | 355 | return Ok(Redirect::to("/login/fediverse/error?reason=unknown").into_response()); |
| 359 | 356 | } |
| 360 | 357 |
@@ -654,7 +651,9 @@ async fn enqueue_auth_decision(
| 654 | 651 | .one(db) |
| 655 | 652 | .await? |
| 656 | 653 | .ok_or_else(|| { |
| 657 | - AppError::Internal(format!("remote_identity {remote_id} disappeared between checks")) | |
| 654 | + AppError::Internal(format!( | |
| 655 | + "remote_identity {remote_id} disappeared between checks" | |
| 656 | + )) | |
| 658 | 657 | })?; |
| 659 | 658 | |
| 660 | 659 | let config = FediverseConfig::from_env() |
@@ -745,17 +744,13 @@ pub async fn connected_apps(
| 745 | 744 | } |
| 746 | 745 | |
| 747 | 746 | // Build the local handle of the current user. |
| 748 | - let domain_self = std::env::var("FEDIVERSE_DOMAIN") | |
| 749 | - .unwrap_or_else(|_| "localhost".to_string()); | |
| 747 | + let domain_self = std::env::var("FEDIVERSE_DOMAIN").unwrap_or_else(|_| "localhost".to_string()); | |
| 750 | 748 | let local_handle = format!("@{}@{}", claims.username, domain_self); |
| 751 | 749 | |
| 752 | 750 | // Pull all `approved` / `consumed` challenges for that handle. |
| 753 | 751 | let challenges = fediverse_auth_challenge::Entity::find() |
| 754 | 752 | .filter(fediverse_auth_challenge::Column::Handle.eq(local_handle)) |
| 755 | - .filter( | |
| 756 | - fediverse_auth_challenge::Column::Status | |
| 757 | - .is_in(["approved", "consumed"]), | |
| 758 | - ) | |
| 753 | + .filter(fediverse_auth_challenge::Column::Status.is_in(["approved", "consumed"])) | |
| 759 | 754 | .order_by_desc(fediverse_auth_challenge::Column::CreatedAt) |
| 760 | 755 | .all(&db) |
| 761 | 756 | .await?; |
@@ -764,9 +759,7 @@ pub async fn connected_apps(
| 764 | 759 | use std::collections::BTreeMap; |
| 765 | 760 | let mut grouped: BTreeMap<String, (u64, chrono::DateTime<chrono::Utc>)> = BTreeMap::new(); |
| 766 | 761 | for c in challenges { |
| 767 | - let entry = grouped | |
| 768 | - .entry(c.domain.clone()) | |
| 769 | - .or_insert((0, c.created_at)); | |
| 762 | + let entry = grouped.entry(c.domain.clone()).or_insert((0, c.created_at)); | |
| 770 | 763 | entry.0 += 1; |
| 771 | 764 | if c.created_at > entry.1 { |
| 772 | 765 | entry.1 = c.created_at; |
M
crates/rustwarden-fediverse-ui/src/lib.rs
+1
-1
@@ -19,5 +19,5 @@ pub mod routes;
| 19 | 19 | pub mod template_examples; |
| 20 | 20 | |
| 21 | 21 | pub use dashboard::dashboard_card; |
| 22 | -pub use dto::{admin_tabs, AdminTabDTO}; | |
| 22 | +pub use dto::{AdminTabDTO, admin_tabs}; | |
| 23 | 23 | pub use routes::default_routes; |
M
crates/rustwarden-fediverse-ui/src/routes.rs
+3
-7
@@ -5,9 +5,9 @@
| 5 | 5 | //! that lived in `rustwarden-ui` so wiring stays a one-liner: |
| 6 | 6 | //! `builder.merge_routes(rustwarden_fediverse_ui::default_routes())`. |
| 7 | 7 | |
| 8 | +use axum::Router; | |
| 8 | 9 | use axum::extract::DefaultBodyLimit; |
| 9 | 10 | use axum::routing::{get, post}; |
| 10 | -use axum::Router; | |
| 11 | 11 | use sea_orm::DatabaseConnection; |
| 12 | 12 | use tower::limit::ConcurrencyLimitLayer; |
| 13 | 13 |
@@ -31,10 +31,7 @@ pub fn default_routes() -> Router<DatabaseConnection> {
| 31 | 31 | Router::new() |
| 32 | 32 | // Federation admin hub (entry point for the six sub-sections). |
| 33 | 33 | .route("/admin/fediverse", get(admin::get_hub)) |
| 34 | - .route( | |
| 35 | - "/admin/fediverse/teams", | |
| 36 | - get(admin::get_teams_for_invites), | |
| 37 | - ) | |
| 34 | + .route("/admin/fediverse/teams", get(admin::get_teams_for_invites)) | |
| 38 | 35 | // Public routes (RFC 0023 Slice B). |
| 39 | 36 | .route("/login/fediverse/error", get(user_auth::error)) |
| 40 | 37 | .route( |
@@ -49,8 +46,7 @@ pub fn default_routes() -> Router<DatabaseConnection> {
| 49 | 46 | ) |
| 50 | 47 | .route( |
| 51 | 48 | "/login/fediverse/sse/{challenge_id}", |
| 52 | - get(user_auth::sse_status) | |
| 53 | - .layer(ConcurrencyLimitLayer::new(SSE_MAX_CONCURRENT)), | |
| 49 | + get(user_auth::sse_status).layer(ConcurrencyLimitLayer::new(SSE_MAX_CONCURRENT)), | |
| 54 | 50 | ) |
| 55 | 51 | // Authenticated routes (consent, invite, connected apps). |
| 56 | 52 | .route( |
M
crates/rustwarden-fediverse/migration/src/lib.rs
+0
-2
@@ -39,5 +39,3 @@ impl MigratorTrait for FediverseMigrator {
| 39 | 39 | ] |
| 40 | 40 | } |
| 41 | 41 | } |
| 42 | - | |
| 43 | - |
M
crates/rustwarden-fediverse/migration/src/m20260512_000001_fediverse_protocol_schema.rs
+11
-7
@@ -80,7 +80,11 @@ impl MigrationTrait for Migration {
| 80 | 80 | .not_null() |
| 81 | 81 | .default("suspend"), |
| 82 | 82 | ) |
| 83 | - .col(ColumnDef::new(FediverseBlockedDomains::Reason).text().null()) | |
| 83 | + .col( | |
| 84 | + ColumnDef::new(FediverseBlockedDomains::Reason) | |
| 85 | + .text() | |
| 86 | + .null(), | |
| 87 | + ) | |
| 84 | 88 | .col( |
| 85 | 89 | ColumnDef::new(FediverseBlockedDomains::AddedByUserId) |
| 86 | 90 | .uuid() |
@@ -123,11 +127,7 @@ impl MigrationTrait for Migration {
| 123 | 127 | value.into(), |
| 124 | 128 | Expr::current_timestamp(), |
| 125 | 129 | ]) |
| 126 | - .on_conflict( | |
| 127 | - OnConflict::column(AppSettings::Key) | |
| 128 | - .do_nothing() | |
| 129 | - .to_owned(), | |
| 130 | - ) | |
| 130 | + .on_conflict(OnConflict::column(AppSettings::Key).do_nothing().to_owned()) | |
| 131 | 131 | .to_owned(); |
| 132 | 132 | manager.get_connection().execute(&insert).await?; |
| 133 | 133 | } |
@@ -181,7 +181,11 @@ impl MigrationTrait for Migration {
| 181 | 181 | .not_null() |
| 182 | 182 | .default(8), |
| 183 | 183 | ) |
| 184 | - .col(ColumnDef::new(FediverseOutboundQueue::LastError).text().null()) | |
| 184 | + .col( | |
| 185 | + ColumnDef::new(FediverseOutboundQueue::LastError) | |
| 186 | + .text() | |
| 187 | + .null(), | |
| 188 | + ) | |
| 185 | 189 | .col( |
| 186 | 190 | ColumnDef::new(FediverseOutboundQueue::NextRetryAt) |
| 187 | 191 | .timestamp_with_time_zone() |
M
crates/rustwarden-fediverse/src/actor.rs
+17
-10
@@ -60,10 +60,7 @@ pub async fn handle(
| 60 | 60 | /// Per Mastodon convention, an absent `Accept` header is treated as a |
| 61 | 61 | /// preference for ActivityPub (AP `id` URLs are the canonical resource). |
| 62 | 62 | pub(crate) fn prefers_activitypub(headers: &HeaderMap) -> bool { |
| 63 | - let Some(value) = headers | |
| 64 | - .get(header::ACCEPT) | |
| 65 | - .and_then(|v| v.to_str().ok()) | |
| 66 | - else { | |
| 63 | + let Some(value) = headers.get(header::ACCEPT).and_then(|v| v.to_str().ok()) else { | |
| 67 | 64 | return true; |
| 68 | 65 | }; |
| 69 | 66 |
@@ -114,10 +111,11 @@ pub(crate) fn build_person_json(
| 114 | 111 | } |
| 115 | 112 | |
| 116 | 113 | fn url_join(base: &Url, path: &str) -> Result<Url, FediverseError> { |
| 117 | - base.join(path) | |
| 118 | - .map_err(|e| FediverseError::App(rustwarden_core::error::AppError::Internal(format!( | |
| 114 | + base.join(path).map_err(|e| { | |
| 115 | + FediverseError::App(rustwarden_core::error::AppError::Internal(format!( | |
| 119 | 116 | "failed to build actor URL from {base} + {path}: {e}" |
| 120 | - )))) | |
| 117 | + ))) | |
| 118 | + }) | |
| 121 | 119 | } |
| 122 | 120 | |
| 123 | 121 | fn append_fragment(url: &Url, fragment: &str) -> Url { |
@@ -155,7 +153,10 @@ mod tests {
| 155 | 153 | async fn count_users_total(&self) -> Result<u64, rustwarden_core::error::AppError> { |
| 156 | 154 | Ok(0) |
| 157 | 155 | } |
| 158 | - async fn count_users_active_since(&self, _days: u32) -> Result<u64, rustwarden_core::error::AppError> { | |
| 156 | + async fn count_users_active_since( | |
| 157 | + &self, | |
| 158 | + _days: u32, | |
| 159 | + ) -> Result<u64, rustwarden_core::error::AppError> { | |
| 159 | 160 | Ok(0) |
| 160 | 161 | } |
| 161 | 162 | async fn count_local_posts(&self) -> Result<u64, rustwarden_core::error::AppError> { |
@@ -223,8 +224,14 @@ mod tests {
| 223 | 224 | assert_eq!(body["preferredUsername"], "alice"); |
| 224 | 225 | assert_eq!(body["inbox"], "https://example.org/users/alice/inbox"); |
| 225 | 226 | assert_eq!(body["outbox"], "https://example.org/users/alice/outbox"); |
| 226 | - assert_eq!(body["followers"], "https://example.org/users/alice/followers"); | |
| 227 | - assert_eq!(body["following"], "https://example.org/users/alice/following"); | |
| 227 | + assert_eq!( | |
| 228 | + body["followers"], | |
| 229 | + "https://example.org/users/alice/followers" | |
| 230 | + ); | |
| 231 | + assert_eq!( | |
| 232 | + body["following"], | |
| 233 | + "https://example.org/users/alice/following" | |
| 234 | + ); | |
| 228 | 235 | assert!(body.get("publicKey").is_none() || body["publicKey"].is_null()); |
| 229 | 236 | } |
| 230 | 237 |
M
crates/rustwarden-fediverse/src/auth_routes.rs
+11
-17
@@ -27,14 +27,14 @@ use uuid::Uuid;
| 27 | 27 | use super::error::FediverseError; |
| 28 | 28 | use super::state::FediverseState; |
| 29 | 29 | use crate::observer::{FediverseEventObserver, NoopFediverseObserver}; |
| 30 | -use rustwarden_core::auth::jwt::Claims; | |
| 31 | -use rustwarden_core::config::auth::AuthConfig; | |
| 30 | +#[cfg(test)] | |
| 31 | +use crate::services::fediverse_auth_service::ChallengeStatus; | |
| 32 | 32 | use crate::services::fediverse_auth_service::{ |
| 33 | 33 | ChallengeDecision, FediverseAuthService, InitiateRequest, |
| 34 | 34 | }; |
| 35 | -#[cfg(test)] | |
| 36 | -use crate::services::fediverse_auth_service::ChallengeStatus; | |
| 37 | 35 | use crate::services::fediverse_signal_service::{SignatureVerifier, VerifyCtx}; |
| 36 | +use rustwarden_core::auth::jwt::Claims; | |
| 37 | +use rustwarden_core::config::auth::AuthConfig; | |
| 38 | 38 | |
| 39 | 39 | /// Per-router state injected into the auth-flow handlers. |
| 40 | 40 | #[derive(Clone)] |
@@ -112,9 +112,8 @@ pub struct ConsentBody {
| 112 | 112 | /// phishing) before the value is persisted into |
| 113 | 113 | /// `fediverse_auth_challenges.redirect_url`. |
| 114 | 114 | fn validate_redirect_url(url_str: &str, allowed_host: &str) -> Result<(), FediverseError> { |
| 115 | - let parsed = url::Url::parse(url_str).map_err(|e| { | |
| 116 | - FediverseError::BadRequest(format!("invalid redirect_url: {e}")) | |
| 117 | - })?; | |
| 115 | + let parsed = url::Url::parse(url_str) | |
| 116 | + .map_err(|e| FediverseError::BadRequest(format!("invalid redirect_url: {e}")))?; | |
| 118 | 117 | match parsed.scheme() { |
| 119 | 118 | "http" | "https" => {} |
| 120 | 119 | other => { |
@@ -197,8 +196,7 @@ async fn consume(
| 197 | 196 | Extension(db): Extension<DatabaseConnection>, |
| 198 | 197 | Path(challenge_id): Path<Uuid>, |
| 199 | 198 | ) -> Result<Json<ConsumeResponse>, FediverseError> { |
| 200 | - let session = | |
| 201 | - FediverseAuthService::consume(&db, &state.auth_config, challenge_id).await?; | |
| 199 | + let session = FediverseAuthService::consume(&db, &state.auth_config, challenge_id).await?; | |
| 202 | 200 | Ok(Json(ConsumeResponse { |
| 203 | 201 | jwt: session.jwt, |
| 204 | 202 | remote_identity_id: session.remote_identity_id, |
@@ -323,9 +321,7 @@ async fn consent(
| 323 | 321 | let decision = match body.decision.as_str() { |
| 324 | 322 | "approve" => { |
| 325 | 323 | let remote = body.remote_identity_id.ok_or_else(|| { |
| 326 | - FediverseError::BadRequest( | |
| 327 | - "approve requires remote_identity_id".into(), | |
| 328 | - ) | |
| 324 | + FediverseError::BadRequest("approve requires remote_identity_id".into()) | |
| 329 | 325 | })?; |
| 330 | 326 | ChallengeDecision::Approve { |
| 331 | 327 | remote_identity_id: remote, |
@@ -364,12 +360,12 @@ pub fn router(state: AuthFlowState) -> Router<DatabaseConnection> {
| 364 | 360 | #[cfg(test)] |
| 365 | 361 | mod tests { |
| 366 | 362 | use super::*; |
| 367 | - use rustwarden_core::error::AppError; | |
| 368 | 363 | use crate::models::fediverse_auth_challenge; |
| 369 | 364 | use async_trait::async_trait; |
| 370 | 365 | use axum::body::Body; |
| 371 | 366 | use axum::http::Request; |
| 372 | 367 | use chrono::{Duration as ChronoDuration, Utc}; |
| 368 | + use rustwarden_core::error::AppError; | |
| 373 | 369 | use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult}; |
| 374 | 370 | use tower::ServiceExt; |
| 375 | 371 |
@@ -544,8 +540,7 @@ mod tests {
| 544 | 540 | |
| 545 | 541 | #[tokio::test] |
| 546 | 542 | async fn initiate_returns_400_for_invalid_handle() { |
| 547 | - let db: DatabaseConnection = | |
| 548 | - MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 543 | + let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 549 | 544 | let body = serde_json::json!({ |
| 550 | 545 | "handle": "no-at-sign", |
| 551 | 546 | }) |
@@ -689,8 +684,7 @@ mod tests {
| 689 | 684 | .method("POST") |
| 690 | 685 | .body(Body::from("{}")) |
| 691 | 686 | .unwrap(); |
| 692 | - let db: DatabaseConnection = | |
| 693 | - MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 687 | + let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 694 | 688 | let resp = build_router(db).oneshot(req).await.unwrap(); |
| 695 | 689 | assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); |
| 696 | 690 | } |
M
crates/rustwarden-fediverse/src/config.rs
+10
-9
@@ -59,7 +59,9 @@ pub enum FediverseConfigError {
| 59 | 59 | PlaceholderDomain(String), |
| 60 | 60 | |
| 61 | 61 | /// Computed base URL is not HTTPS while `debug_local_federation` is off. |
| 62 | - #[error("base URL `{0}` must use HTTPS in production (set FEDIVERSE_DEBUG_LOCAL_FEDERATION=true for local tests)")] | |
| 62 | + #[error( | |
| 63 | + "base URL `{0}` must use HTTPS in production (set FEDIVERSE_DEBUG_LOCAL_FEDERATION=true for local tests)" | |
| 64 | + )] | |
| 63 | 65 | InsecureBaseUrl(String), |
| 64 | 66 | |
| 65 | 67 | /// `FEDIVERSE_DOMAIN` cannot be parsed as a valid URL host. |
@@ -142,8 +144,10 @@ impl FediverseConfig {
| 142 | 144 | let authorized_fetch = read_bool_env("FEDIVERSE_AUTHORIZED_FETCH", false)?; |
| 143 | 145 | let outbound_workers = read_numeric_env("FEDIVERSE_OUTBOUND_WORKERS", 4)?; |
| 144 | 146 | let outbound_max_attempts = read_numeric_env("FEDIVERSE_OUTBOUND_MAX_ATTEMPTS", 8)?; |
| 145 | - let outbound_request_timeout = | |
| 146 | - Duration::from_secs(read_numeric_env("FEDIVERSE_OUTBOUND_REQUEST_TIMEOUT_SECS", 30)?); | |
| 147 | + let outbound_request_timeout = Duration::from_secs(read_numeric_env( | |
| 148 | + "FEDIVERSE_OUTBOUND_REQUEST_TIMEOUT_SECS", | |
| 149 | + 30, | |
| 150 | + )?); | |
| 147 | 151 | let fetch_actor_cache_ttl = Duration::from_secs(read_numeric_env( |
| 148 | 152 | "FEDIVERSE_FETCH_ACTOR_CACHE_TTL_SECS", |
| 149 | 153 | 86_400, |
@@ -151,11 +155,7 @@ impl FediverseConfig {
| 151 | 155 | let auth_challenge_ttl = |
| 152 | 156 | Duration::from_secs(read_numeric_env("FEDIVERSE_AUTH_CHALLENGE_TTL_SECS", 300)?); |
| 153 | 157 | |
| 154 | - let user_agent = format!( | |
| 155 | - "rustwarden/{} (+{})", | |
| 156 | - env!("CARGO_PKG_VERSION"), | |
| 157 | - base_url | |
| 158 | - ); | |
| 158 | + let user_agent = format!("rustwarden/{} (+{})", env!("CARGO_PKG_VERSION"), base_url); | |
| 159 | 159 | |
| 160 | 160 | Ok(Self { |
| 161 | 161 | domain: host_part, |
@@ -481,7 +481,8 @@ mod tests {
| 481 | 481 | assert!(!config.authorized_fetch()); |
| 482 | 482 | assert!(!config.debug_local_federation()); |
| 483 | 483 | assert!( |
| 484 | - config.user_agent().contains("rustwarden") || config.user_agent().contains("Rustwarden"), | |
| 484 | + config.user_agent().contains("rustwarden") | |
| 485 | + || config.user_agent().contains("Rustwarden"), | |
| 485 | 486 | "user_agent={}", |
| 486 | 487 | config.user_agent() |
| 487 | 488 | ); |
M
crates/rustwarden-fediverse/src/fetcher.rs
+12
-8
@@ -49,16 +49,20 @@ pub struct UnconfiguredFetcher;
| 49 | 49 | #[async_trait] |
| 50 | 50 | impl RemoteFetcher for UnconfiguredFetcher { |
| 51 | 51 | async fn webfinger(&self, _handle: &str) -> Result<WebFingerResponse, FediverseError> { |
| 52 | - Err(FediverseError::App(rustwarden_core::error::AppError::Config( | |
| 53 | - "RemoteFetcher not configured — wire a real implementation via FediverseState" | |
| 54 | - .to_string(), | |
| 55 | - ))) | |
| 52 | + Err(FediverseError::App( | |
| 53 | + rustwarden_core::error::AppError::Config( | |
| 54 | + "RemoteFetcher not configured — wire a real implementation via FediverseState" | |
| 55 | + .to_string(), | |
| 56 | + ), | |
| 57 | + )) | |
| 56 | 58 | } |
| 57 | 59 | |
| 58 | 60 | async fn fetch_actor(&self, _actor_url: &Url) -> Result<PersonJson, FediverseError> { |
| 59 | - Err(FediverseError::App(rustwarden_core::error::AppError::Config( | |
| 60 | - "RemoteFetcher not configured — wire a real implementation via FediverseState" | |
| 61 | - .to_string(), | |
| 62 | - ))) | |
| 61 | + Err(FediverseError::App( | |
| 62 | + rustwarden_core::error::AppError::Config( | |
| 63 | + "RemoteFetcher not configured — wire a real implementation via FediverseState" | |
| 64 | + .to_string(), | |
| 65 | + ), | |
| 66 | + )) | |
| 63 | 67 | } |
| 64 | 68 | } |
M
crates/rustwarden-fediverse/src/http_signatures.rs
+52
-92
@@ -25,12 +25,12 @@ use sha2::Sha256;
| 25 | 25 | use async_trait::async_trait; |
| 26 | 26 | use sea_orm::DatabaseConnection; |
| 27 | 27 | |
| 28 | -use rustwarden_core::error::AppError; | |
| 29 | 28 | use crate::models::fediverse_outbound_queue; |
| 30 | 29 | use crate::observer::{FediverseEventObserver, NoopFediverseObserver}; |
| 31 | 30 | use crate::services::actor_key_service::ActorKeyService; |
| 32 | 31 | use crate::services::fediverse_outbound_service::OutboundDelivery; |
| 33 | 32 | use crate::services::fediverse_signal_service::{SignatureVerifier, VerifyCtx}; |
| 33 | +use rustwarden_core::error::AppError; | |
| 34 | 34 | |
| 35 | 35 | /// Canonical fields extracted from a draft-cavage `Signature` header. |
| 36 | 36 | #[derive(Debug, Clone, PartialEq, Eq)] |
@@ -83,9 +83,7 @@ pub fn parse_signature_header(raw: &str) -> Result<ParsedSignature, AppError> {
| 83 | 83 | |
| 84 | 84 | let signature = base64::engine::general_purpose::STANDARD |
| 85 | 85 | .decode(&signature_b64) |
| 86 | - .map_err(|e| { | |
| 87 | - AppError::Validation(format!("Signature base64 decode failed: {e}")) | |
| 88 | - })?; | |
| 86 | + .map_err(|e| AppError::Validation(format!("Signature base64 decode failed: {e}")))?; | |
| 89 | 87 | |
| 90 | 88 | Ok(ParsedSignature { |
| 91 | 89 | key_id, |
@@ -134,11 +132,7 @@ pub fn rsa_sha256_sign(private_pem: &str, input: &[u8]) -> Result<Vec<u8>, AppEr
| 134 | 132 | } |
| 135 | 133 | |
| 136 | 134 | /// Verify an RSA-SHA256 PKCS#1 v1.5 signature against `input`. |
| 137 | -pub fn rsa_sha256_verify( | |
| 138 | - public_pem: &str, | |
| 139 | - input: &[u8], | |
| 140 | - signature: &[u8], | |
| 141 | -) -> Result<(), AppError> { | |
| 135 | +pub fn rsa_sha256_verify(public_pem: &str, input: &[u8], signature: &[u8]) -> Result<(), AppError> { | |
| 142 | 136 | let public_key = RsaPublicKey::from_public_key_pem(public_pem) |
| 143 | 137 | .map_err(|e| AppError::Internal(format!("invalid SPKI PEM: {e}")))?; |
| 144 | 138 | let verifying_key = VerifyingKey::<Sha256>::new(public_key); |
@@ -175,9 +169,7 @@ pub fn parse_and_verify_cavage(
| 175 | 169 | // activities (Delete, Update, Revoke, …). |
| 176 | 170 | if parsed.headers.iter().any(|h| h == "date") { |
| 177 | 171 | let date_value = headers("date").ok_or_else(|| { |
| 178 | - AppError::Validation( | |
| 179 | - "Signature covers 'date' but the Date header is absent".into(), | |
| 180 | - ) | |
| 172 | + AppError::Validation("Signature covers 'date' but the Date header is absent".into()) | |
| 181 | 173 | })?; |
| 182 | 174 | validate_date_header(&date_value, MAX_DATE_SKEW_SECONDS)?; |
| 183 | 175 | } |
@@ -188,9 +180,7 @@ pub fn parse_and_verify_cavage(
| 188 | 180 | // body while keeping the signed headers intact (MITM / log replay). |
| 189 | 181 | if parsed.headers.iter().any(|h| h == "digest") { |
| 190 | 182 | let digest_value = headers("digest").ok_or_else(|| { |
| 191 | - AppError::Validation( | |
| 192 | - "Signature covers 'digest' but the Digest header is absent".into(), | |
| 193 | - ) | |
| 183 | + AppError::Validation("Signature covers 'digest' but the Digest header is absent".into()) | |
| 194 | 184 | })?; |
| 195 | 185 | validate_digest(&digest_value, body)?; |
| 196 | 186 | } |
@@ -323,10 +313,7 @@ impl SignatureVerifier for HttpSigVerifier {
| 323 | 313 | .one(db) |
| 324 | 314 | .await? |
| 325 | 315 | .ok_or_else(|| { |
| 326 | - AppError::Validation(format!( | |
| 327 | - "actor not in cache: {}", | |
| 328 | - ctx.actor_ap_id, | |
| 329 | - )) | |
| 316 | + AppError::Validation(format!("actor not in cache: {}", ctx.actor_ap_id,)) | |
| 330 | 317 | })?; |
| 331 | 318 | resolved_pem = row.public_key_pem; |
| 332 | 319 | &resolved_pem |
@@ -385,19 +372,13 @@ impl SignatureVerifier for HttpSigVerifier {
| 385 | 372 | // RFC 0015 / ANSSI F-CRYPTO-2 — Date skew detection. |
| 386 | 373 | if parsed.headers.iter().any(|h| h == "date") { |
| 387 | 374 | let date_value = header_lookup("date").ok_or_else(|| { |
| 388 | - AppError::Validation( | |
| 389 | - "Signature covers 'date' but the Date header is absent".into(), | |
| 390 | - ) | |
| 375 | + AppError::Validation("Signature covers 'date' but the Date header is absent".into()) | |
| 391 | 376 | })?; |
| 392 | 377 | if let Err((skew, err)) = |
| 393 | 378 | validate_date_header_signed(&date_value, MAX_DATE_SKEW_SECONDS) |
| 394 | 379 | { |
| 395 | 380 | self.observer |
| 396 | - .on_replay_attempt( | |
| 397 | - &domain, | |
| 398 | - actor_uri, | |
| 399 | - serde_json::json!({"skew_secs": skew}), | |
| 400 | - ) | |
| 381 | + .on_replay_attempt(&domain, actor_uri, serde_json::json!({"skew_secs": skew})) | |
| 401 | 382 | .await; |
| 402 | 383 | return Err(err); |
| 403 | 384 | } |
@@ -422,11 +403,8 @@ impl SignatureVerifier for HttpSigVerifier {
| 422 | 403 | } |
| 423 | 404 | } |
| 424 | 405 | |
| 425 | - let input = | |
| 426 | - build_signature_input(&parsed.headers, &request_target, header_lookup)?; | |
| 427 | - if let Err(err) = | |
| 428 | - rsa_sha256_verify(public_key_pem, input.as_bytes(), &parsed.signature) | |
| 429 | - { | |
| 406 | + let input = build_signature_input(&parsed.headers, &request_target, header_lookup)?; | |
| 407 | + if let Err(err) = rsa_sha256_verify(public_key_pem, input.as_bytes(), &parsed.signature) { | |
| 430 | 408 | self.observer |
| 431 | 409 | .on_signature_failure(&domain, actor_uri, "bad_signature") |
| 432 | 410 | .await; |
@@ -449,10 +427,7 @@ fn extract_actor_domain(actor_ap_id: &str) -> String {
| 449 | 427 | /// Variant of [`validate_date_header`] that returns the measured skew |
| 450 | 428 | /// alongside the error so that the observer can emit it as structured |
| 451 | 429 | /// JSON. |
| 452 | -fn validate_date_header_signed( | |
| 453 | - date_str: &str, | |
| 454 | - skew_seconds: i64, | |
| 455 | -) -> Result<(), (i64, AppError)> { | |
| 430 | +fn validate_date_header_signed(date_str: &str, skew_seconds: i64) -> Result<(), (i64, AppError)> { | |
| 456 | 431 | let parsed = chrono::DateTime::parse_from_rfc2822(date_str.trim()) |
| 457 | 432 | .map_err(|e| (0, AppError::Validation(format!("invalid Date header: {e}"))))?; |
| 458 | 433 | let diff = (chrono::Utc::now() - parsed.with_timezone(&chrono::Utc)) |
@@ -492,28 +467,24 @@ impl HttpSignedDelivery {
| 492 | 467 | |
| 493 | 468 | #[async_trait] |
| 494 | 469 | impl OutboundDelivery for HttpSignedDelivery { |
| 495 | - async fn deliver( | |
| 496 | - &self, | |
| 497 | - item: &fediverse_outbound_queue::Model, | |
| 498 | - ) -> Result<(), AppError> { | |
| 499 | - let actor_user_id = item.actor_user_id.ok_or_else(|| { | |
| 500 | - AppError::Config("HttpSignedDelivery requires actor_user_id".into()) | |
| 501 | - })?; | |
| 470 | + async fn deliver(&self, item: &fediverse_outbound_queue::Model) -> Result<(), AppError> { | |
| 471 | + let actor_user_id = item | |
| 472 | + .actor_user_id | |
| 473 | + .ok_or_else(|| AppError::Config("HttpSignedDelivery requires actor_user_id".into()))?; | |
| 502 | 474 | |
| 503 | 475 | let material = ActorKeyService::ensure_keys(&self.db, actor_user_id).await?; |
| 504 | 476 | |
| 505 | - let url = url::Url::parse(&item.target_inbox).map_err(|e| { | |
| 506 | - AppError::Internal(format!("invalid target_inbox URL: {e}")) | |
| 507 | - })?; | |
| 477 | + let url = url::Url::parse(&item.target_inbox) | |
| 478 | + .map_err(|e| AppError::Internal(format!("invalid target_inbox URL: {e}")))?; | |
| 508 | 479 | let host = url |
| 509 | 480 | .host_str() |
| 510 | - .ok_or_else(|| { | |
| 511 | - AppError::Internal("target_inbox URL has no host".to_string()) | |
| 512 | - })? | |
| 481 | + .ok_or_else(|| AppError::Internal("target_inbox URL has no host".to_string()))? | |
| 513 | 482 | .to_string(); |
| 514 | 483 | let path = url.path().to_string(); |
| 515 | 484 | |
| 516 | - let date = chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string(); | |
| 485 | + let date = chrono::Utc::now() | |
| 486 | + .format("%a, %d %b %Y %H:%M:%S GMT") | |
| 487 | + .to_string(); | |
| 517 | 488 | let signed_headers = vec![ |
| 518 | 489 | "(request-target)".to_string(), |
| 519 | 490 | "host".to_string(), |
@@ -527,8 +498,7 @@ impl OutboundDelivery for HttpSignedDelivery {
| 527 | 498 | })?; |
| 528 | 499 | |
| 529 | 500 | let signature_bytes = rsa_sha256_sign(&material.private_pem, input.as_bytes())?; |
| 530 | - let signature_b64 = | |
| 531 | - base64::engine::general_purpose::STANDARD.encode(&signature_bytes); | |
| 501 | + let signature_b64 = base64::engine::general_purpose::STANDARD.encode(&signature_bytes); | |
| 532 | 502 | let signature_header = format!( |
| 533 | 503 | r#"keyId="{}#main-key",algorithm="rsa-sha256",headers="(request-target) host date",signature="{signature_b64}""#, |
| 534 | 504 | item.target_inbox.trim_end_matches('/'), |
@@ -678,8 +648,7 @@ mod tests {
| 678 | 648 | #[test] |
| 679 | 649 | fn parse_and_verify_cavage_rejects_unsupported_algorithm() { |
| 680 | 650 | let header = r#"keyId="x",algorithm="hs2019",headers="host",signature="dGVzdA==""#; |
| 681 | - let err = parse_and_verify_cavage(header, "post /x", b"", "", |_| None) | |
| 682 | - .expect_err(""); | |
| 651 | + let err = parse_and_verify_cavage(header, "post /x", b"", "", |_| None).expect_err(""); | |
| 683 | 652 | assert!(matches!(err, AppError::Validation(_))); |
| 684 | 653 | } |
| 685 | 654 |
@@ -738,13 +707,12 @@ mod tests {
| 738 | 707 | let date = chrono::Utc::now() |
| 739 | 708 | .format("%a, %d %b %Y %H:%M:%S GMT") |
| 740 | 709 | .to_string(); |
| 741 | - let input = | |
| 742 | - build_signature_input(&signed_headers, "post /inbox", |h| match h { | |
| 743 | - "host" => Some("b.tld".into()), | |
| 744 | - "date" => Some(date.clone()), | |
| 745 | - _ => None, | |
| 746 | - }) | |
| 747 | - .unwrap(); | |
| 710 | + let input = build_signature_input(&signed_headers, "post /inbox", |h| match h { | |
| 711 | + "host" => Some("b.tld".into()), | |
| 712 | + "date" => Some(date.clone()), | |
| 713 | + _ => None, | |
| 714 | + }) | |
| 715 | + .unwrap(); | |
| 748 | 716 | let sig_bytes = rsa_sha256_sign(&priv_pem, input.as_bytes()).unwrap(); |
| 749 | 717 | let header_value = format!( |
| 750 | 718 | r#"keyId="x",algorithm="rsa-sha256",headers="(request-target) host date",signature="{}""#, |
@@ -785,13 +753,12 @@ mod tests {
| 785 | 753 | let date = chrono::Utc::now() |
| 786 | 754 | .format("%a, %d %b %Y %H:%M:%S GMT") |
| 787 | 755 | .to_string(); |
| 788 | - let input = | |
| 789 | - build_signature_input(&signed_headers, "post /inbox", |h| match h { | |
| 790 | - "host" => Some("b.tld".into()), | |
| 791 | - "date" => Some(date.clone()), | |
| 792 | - _ => None, | |
| 793 | - }) | |
| 794 | - .unwrap(); | |
| 756 | + let input = build_signature_input(&signed_headers, "post /inbox", |h| match h { | |
| 757 | + "host" => Some("b.tld".into()), | |
| 758 | + "date" => Some(date.clone()), | |
| 759 | + _ => None, | |
| 760 | + }) | |
| 761 | + .unwrap(); | |
| 795 | 762 | let sig_bytes = rsa_sha256_sign(&priv_pem, input.as_bytes()).unwrap(); |
| 796 | 763 | let header_value = format!( |
| 797 | 764 | r#"keyId="x",algorithm="rsa-sha256",headers="(request-target) host date",signature="{}""#, |
@@ -847,7 +814,9 @@ mod tests {
| 847 | 814 | let mut headers = HeaderMap::new(); |
| 848 | 815 | headers.insert( |
| 849 | 816 | "signature", |
| 850 | - HeaderValue::from_static(r#"keyId="x",algorithm="rsa-sha256",headers="host",signature="dGVzdA==""#), | |
| 817 | + HeaderValue::from_static( | |
| 818 | + r#"keyId="x",algorithm="rsa-sha256",headers="host",signature="dGVzdA==""#, | |
| 819 | + ), | |
| 851 | 820 | ); |
| 852 | 821 | let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres) |
| 853 | 822 | .append_query_results([Vec::<crate::models::remote_identity::Model>::new()]) |
@@ -874,8 +843,7 @@ mod tests {
| 874 | 843 | use chrono::Utc; |
| 875 | 844 | use sea_orm::{DatabaseBackend, MockDatabase}; |
| 876 | 845 | use uuid::Uuid; |
| 877 | - let db: DatabaseConnection = | |
| 878 | - MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 846 | + let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 879 | 847 | let now = Utc::now(); |
| 880 | 848 | let item = Model { |
| 881 | 849 | id: Uuid::new_v4(), |
@@ -932,8 +900,7 @@ mod tests {
| 932 | 900 | fn validate_digest_accepts_matching_sha256() { |
| 933 | 901 | use sha2::Digest; |
| 934 | 902 | let body = b"hello world"; |
| 935 | - let expected = base64::engine::general_purpose::STANDARD | |
| 936 | - .encode(sha2::Sha256::digest(body)); | |
| 903 | + let expected = base64::engine::general_purpose::STANDARD.encode(sha2::Sha256::digest(body)); | |
| 937 | 904 | let header = format!("SHA-256={expected}"); |
| 938 | 905 | validate_digest(&header, body).expect("matching digest must succeed"); |
| 939 | 906 | } |
@@ -943,8 +910,7 @@ mod tests {
| 943 | 910 | use sha2::Digest; |
| 944 | 911 | let header = format!( |
| 945 | 912 | "SHA-256={}", |
| 946 | - base64::engine::general_purpose::STANDARD | |
| 947 | - .encode(sha2::Sha256::digest(b"signed body")) | |
| 913 | + base64::engine::general_purpose::STANDARD.encode(sha2::Sha256::digest(b"signed body")) | |
| 948 | 914 | ); |
| 949 | 915 | let err = validate_digest(&header, b"tampered body") |
| 950 | 916 | .expect_err("mismatched digest must be rejected"); |
@@ -953,15 +919,15 @@ mod tests {
| 953 | 919 | |
| 954 | 920 | #[test] |
| 955 | 921 | fn validate_digest_rejects_unsupported_algorithm() { |
| 956 | - let err = validate_digest("MD5=abc", b"") | |
| 957 | - .expect_err("unsupported algorithm must be rejected"); | |
| 922 | + let err = | |
| 923 | + validate_digest("MD5=abc", b"").expect_err("unsupported algorithm must be rejected"); | |
| 958 | 924 | assert!(format!("{err}").contains("unsupported")); |
| 959 | 925 | } |
| 960 | 926 | |
| 961 | 927 | #[test] |
| 962 | 928 | fn validate_digest_rejects_malformed_header() { |
| 963 | - let err = validate_digest("no-equals-sign", b"") | |
| 964 | - .expect_err("malformed digest must be rejected"); | |
| 929 | + let err = | |
| 930 | + validate_digest("no-equals-sign", b"").expect_err("malformed digest must be rejected"); | |
| 965 | 931 | assert!(format!("{err}").contains("malformed")); |
| 966 | 932 | } |
| 967 | 933 |
@@ -972,8 +938,7 @@ mod tests {
| 972 | 938 | let now = chrono::Utc::now() |
| 973 | 939 | .format("%a, %d %b %Y %H:%M:%S GMT") |
| 974 | 940 | .to_string(); |
| 975 | - validate_date_header(&now, MAX_DATE_SKEW_SECONDS) | |
| 976 | - .expect("recent date must succeed"); | |
| 941 | + validate_date_header(&now, MAX_DATE_SKEW_SECONDS).expect("recent date must succeed"); | |
| 977 | 942 | } |
| 978 | 943 | |
| 979 | 944 | #[test] |
@@ -1016,12 +981,7 @@ mod tests {
| 1016 | 981 | |
| 1017 | 982 | #[async_trait] |
| 1018 | 983 | impl FediverseEventObserver for CountingObserver { |
| 1019 | - async fn on_signature_failure( | |
| 1020 | - &self, | |
| 1021 | - _domain: &str, | |
| 1022 | - _actor: Option<&str>, | |
| 1023 | - _reason: &str, | |
| 1024 | - ) { | |
| 984 | + async fn on_signature_failure(&self, _domain: &str, _actor: Option<&str>, _reason: &str) { | |
| 1025 | 985 | self.sig_failure.fetch_add(1, Ordering::SeqCst); |
| 1026 | 986 | } |
| 1027 | 987 | async fn on_replay_attempt( |
@@ -1090,7 +1050,10 @@ mod tests {
| 1090 | 1050 | public_key_pem: &pub_pem, |
| 1091 | 1051 | ..ctx |
| 1092 | 1052 | }; |
| 1093 | - let err = verifier.verify(&ctx).await.expect_err("stale date must reject"); | |
| 1053 | + let err = verifier | |
| 1054 | + .verify(&ctx) | |
| 1055 | + .await | |
| 1056 | + .expect_err("stale date must reject"); | |
| 1094 | 1057 | assert!(matches!(err, AppError::Validation(_))); |
| 1095 | 1058 | assert_eq!(observer.replay.load(Ordering::SeqCst), 1); |
| 1096 | 1059 | let details = observer.last_details.lock().unwrap().clone().unwrap(); |
@@ -1113,10 +1076,7 @@ mod tests {
| 1113 | 1076 | ]; |
| 1114 | 1077 | // Sign over a digest of "signed body" but send "tampered body" |
| 1115 | 1078 | // so the Digest header value mismatches the actual body bytes. |
| 1116 | - let fake_digest = format!( | |
| 1117 | - "SHA-256={}", | |
| 1118 | - b64(&sha2::Sha256::digest(b"signed body")) | |
| 1119 | - ); | |
| 1079 | + let fake_digest = format!("SHA-256={}", b64(&sha2::Sha256::digest(b"signed body"))); | |
| 1120 | 1080 | let input = build_signature_input(&signed, "post /inbox", |h| match h { |
| 1121 | 1081 | "host" => Some("b.tld".into()), |
| 1122 | 1082 | "date" => Some(date.clone()), |
M
crates/rustwarden-fediverse/src/inbox.rs
+14
-29
@@ -63,9 +63,7 @@ pub async fn handle(
| 63 | 63 | let signature_header = headers |
| 64 | 64 | .get("signature") |
| 65 | 65 | .and_then(|v| v.to_str().ok()) |
| 66 | - .ok_or_else(|| { | |
| 67 | - FediverseError::Unauthorized("missing Signature header".to_string()) | |
| 68 | - })?; | |
| 66 | + .ok_or_else(|| FediverseError::Unauthorized("missing Signature header".to_string()))?; | |
| 69 | 67 | |
| 70 | 68 | let activity: InboxActivity = match serde_json::from_slice(&body) { |
| 71 | 69 | Ok(a) => a, |
@@ -76,9 +74,7 @@ pub async fn handle(
| 76 | 74 | // means we can't even read the actor URL, so we fall back to |
| 77 | 75 | // the empty string which the observer-side normaliser |
| 78 | 76 | // suppresses. |
| 79 | - observer | |
| 80 | - .on_inbox_abuse("", None, "malformed_json") | |
| 81 | - .await; | |
| 77 | + observer.on_inbox_abuse("", None, "malformed_json").await; | |
| 82 | 78 | return Err(FediverseError::BadRequest(format!("invalid JSON: {e}"))); |
| 83 | 79 | } |
| 84 | 80 | }; |
@@ -120,11 +116,7 @@ pub async fn handle(
| 120 | 116 | "Revoke" => dispatch_revoke(&db, &activity).await?, |
| 121 | 117 | other => { |
| 122 | 118 | observer |
| 123 | - .on_inbox_abuse( | |
| 124 | - &source_domain, | |
| 125 | - Some(actor_ap_id), | |
| 126 | - "unknown_activity_type", | |
| 127 | - ) | |
| 119 | + .on_inbox_abuse(&source_domain, Some(actor_ap_id), "unknown_activity_type") | |
| 128 | 120 | .await; |
| 129 | 121 | return Err(FediverseError::Unsupported(other.to_string())); |
| 130 | 122 | } |
@@ -231,7 +223,9 @@ async fn dispatch_revoke(
| 231 | 223 | .get("subject") |
| 232 | 224 | .and_then(|v| v.as_str()) |
| 233 | 225 | .and_then(|s| Uuid::parse_str(s).ok()) |
| 234 | - .ok_or_else(|| FediverseError::BadRequest("Revoke.object.subject missing/malformed".into()))?; | |
| 226 | + .ok_or_else(|| { | |
| 227 | + FediverseError::BadRequest("Revoke.object.subject missing/malformed".into()) | |
| 228 | + })?; | |
| 235 | 229 | |
| 236 | 230 | FediverseSignalService::apply_revoke_membership(db, team_id, remote_identity_id).await?; |
| 237 | 231 | Ok(()) |
@@ -240,9 +234,9 @@ async fn dispatch_revoke(
| 240 | 234 | #[cfg(test)] |
| 241 | 235 | mod tests { |
| 242 | 236 | use super::*; |
| 243 | - use rustwarden_core::error::AppError; | |
| 244 | 237 | use async_trait::async_trait; |
| 245 | 238 | use axum::{Router, body::Body, http::Request, routing::post}; |
| 239 | + use rustwarden_core::error::AppError; | |
| 246 | 240 | use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult}; |
| 247 | 241 | use tower::ServiceExt; |
| 248 | 242 |
@@ -486,26 +480,17 @@ mod tests {
| 486 | 480 | |
| 487 | 481 | #[async_trait] |
| 488 | 482 | impl FediverseEventObserver for CountingObserver { |
| 489 | - async fn on_inbox_abuse( | |
| 490 | - &self, | |
| 491 | - _domain: &str, | |
| 492 | - _actor: Option<&str>, | |
| 493 | - reason: &str, | |
| 494 | - ) { | |
| 483 | + async fn on_inbox_abuse(&self, _domain: &str, _actor: Option<&str>, reason: &str) { | |
| 495 | 484 | self.calls.fetch_add(1, Ordering::SeqCst); |
| 496 | 485 | self.reasons.lock().unwrap().push(reason.to_string()); |
| 497 | 486 | } |
| 498 | 487 | |
| 499 | - async fn on_delete_actor( | |
| 500 | - &self, | |
| 501 | - domain: &str, | |
| 502 | - ap_id: &str, | |
| 503 | - remote_identity_id: Uuid, | |
| 504 | - ) { | |
| 505 | - self.delete_calls | |
| 506 | - .lock() | |
| 507 | - .unwrap() | |
| 508 | - .push((domain.to_string(), ap_id.to_string(), remote_identity_id)); | |
| 488 | + async fn on_delete_actor(&self, domain: &str, ap_id: &str, remote_identity_id: Uuid) { | |
| 489 | + self.delete_calls.lock().unwrap().push(( | |
| 490 | + domain.to_string(), | |
| 491 | + ap_id.to_string(), | |
| 492 | + remote_identity_id, | |
| 493 | + )); | |
| 509 | 494 | } |
| 510 | 495 | } |
| 511 | 496 |
M
crates/rustwarden-fediverse/src/invite_routes.rs
+14
-8
@@ -27,9 +27,9 @@ use serde::{Deserialize, Serialize};
| 27 | 27 | use uuid::Uuid; |
| 28 | 28 | |
| 29 | 29 | use super::error::FediverseError; |
| 30 | +use crate::services::fediverse_invite_service::FediverseInviteService; | |
| 30 | 31 | use rustwarden_core::auth::jwt::Claims; |
| 31 | 32 | use rustwarden_core::error::AppError; |
| 32 | -use crate::services::fediverse_invite_service::FediverseInviteService; | |
| 33 | 33 | use rustwarden_core::services::team_service::TeamRole; |
| 34 | 34 | |
| 35 | 35 | /// Per-router state injected into the invite handlers. |
@@ -170,8 +170,7 @@ async fn idp_list(
| 170 | 170 | ) -> Result<Json<Vec<InviteRow>>, FediverseError> { |
| 171 | 171 | // ANSSI F-NET-3 — IdP-side listing is now authenticated; future RFC |
| 172 | 172 | // will narrow `handle` to the caller's own bound identity. |
| 173 | - let rows = | |
| 174 | - FediverseInviteService::list_pending_for_handle(&db, &query.handle).await?; | |
| 173 | + let rows = FediverseInviteService::list_pending_for_handle(&db, &query.handle).await?; | |
| 175 | 174 | Ok(Json(rows.into_iter().map(into_row).collect())) |
| 176 | 175 | } |
| 177 | 176 |
@@ -352,8 +351,7 @@ mod tests {
| 352 | 351 | #[tokio::test] |
| 353 | 352 | async fn invite_returns_400_for_invalid_role() { |
| 354 | 353 | let team_id = Uuid::new_v4(); |
| 355 | - let db: DatabaseConnection = | |
| 356 | - MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 354 | + let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 357 | 355 | let body = serde_json::json!({"handle":"alice@a.tld","role":"superuser"}).to_string(); |
| 358 | 356 | let resp = build_router(db) |
| 359 | 357 | .oneshot(req_post( |
@@ -443,7 +441,9 @@ mod tests {
| 443 | 441 | .append_query_results([Vec::<fediverse_pending_invite::Model>::new()]) |
| 444 | 442 | .into_connection(); |
| 445 | 443 | let resp = build_router(db) |
| 446 | - .oneshot(req_get("/api/v1/fediverse/invites?handle=ghost@nowhere.tld")) | |
| 444 | + .oneshot(req_get( | |
| 445 | + "/api/v1/fediverse/invites?handle=ghost@nowhere.tld", | |
| 446 | + )) | |
| 447 | 447 | .await |
| 448 | 448 | .unwrap(); |
| 449 | 449 | assert_eq!(resp.status(), StatusCode::OK); |
@@ -472,8 +472,14 @@ mod tests {
| 472 | 472 | .append_query_results([vec![after_invite]]) |
| 473 | 473 | .append_query_results([vec![new_member]]) |
| 474 | 474 | .append_exec_results([ |
| 475 | - MockExecResult { last_insert_id: 0, rows_affected: 1 }, | |
| 476 | - MockExecResult { last_insert_id: 0, rows_affected: 1 }, | |
| 475 | + MockExecResult { | |
| 476 | + last_insert_id: 0, | |
| 477 | + rows_affected: 1, | |
| 478 | + }, | |
| 479 | + MockExecResult { | |
| 480 | + last_insert_id: 0, | |
| 481 | + rows_affected: 1, | |
| 482 | + }, | |
| 477 | 483 | ]) |
| 478 | 484 | .into_connection(); |
| 479 | 485 | let body = serde_json::json!({"remote_identity_id":remote_id}).to_string(); |
M
crates/rustwarden-fediverse/src/nodeinfo.rs
+4
-1
@@ -92,7 +92,10 @@ mod tests {
| 92 | 92 | async fn count_users_total(&self) -> Result<u64, rustwarden_core::error::AppError> { |
| 93 | 93 | Ok(self.total) |
| 94 | 94 | } |
| 95 | - async fn count_users_active_since(&self, days: u32) -> Result<u64, rustwarden_core::error::AppError> { | |
| 95 | + async fn count_users_active_since( | |
| 96 | + &self, | |
| 97 | + days: u32, | |
| 98 | + ) -> Result<u64, rustwarden_core::error::AppError> { | |
| 96 | 99 | Ok(if days <= 30 { |
| 97 | 100 | self.active_month |
| 98 | 101 | } else { |
M
crates/rustwarden-fediverse/src/observer.rs
+7
-30
@@ -38,24 +38,12 @@ pub trait FediverseEventObserver: Send + Sync {
| 38 | 38 | /// from `keyId`). `actor_uri` is the full AP id when available. |
| 39 | 39 | /// `reason` is a short machine-stable string ("digest mismatch", |
| 40 | 40 | /// "bad signature", "key not in cache", …). |
| 41 | - async fn on_signature_failure( | |
| 42 | - &self, | |
| 43 | - _domain: &str, | |
| 44 | - _actor_uri: Option<&str>, | |
| 45 | - _reason: &str, | |
| 46 | - ) { | |
| 47 | - } | |
| 41 | + async fn on_signature_failure(&self, _domain: &str, _actor_uri: Option<&str>, _reason: &str) {} | |
| 48 | 42 | |
| 49 | 43 | /// A replay was detected (stale `Date` header, duplicate nonce, |
| 50 | 44 | /// or signature re-use). `details` is a free-form JSON object |
| 51 | 45 | /// the detector can use to enrich logs. |
| 52 | - async fn on_replay_attempt( | |
| 53 | - &self, | |
| 54 | - _domain: &str, | |
| 55 | - _actor_uri: Option<&str>, | |
| 56 | - _details: Value, | |
| 57 | - ) { | |
| 58 | - } | |
| 46 | + async fn on_replay_attempt(&self, _domain: &str, _actor_uri: Option<&str>, _details: Value) {} | |
| 59 | 47 | |
| 60 | 48 | /// Local outbound fetcher refused to talk to a private / loopback |
| 61 | 49 | /// / link-local IP (SSRF guard fired). `target_host` is the host |
@@ -64,13 +52,7 @@ pub trait FediverseEventObserver: Send + Sync {
| 64 | 52 | |
| 65 | 53 | /// An inbox payload failed structural validation (body too large, |
| 66 | 54 | /// schema rejected, unknown activity type, …). |
| 67 | - async fn on_inbox_abuse( | |
| 68 | - &self, | |
| 69 | - _domain: &str, | |
| 70 | - _actor_uri: Option<&str>, | |
| 71 | - _reason: &str, | |
| 72 | - ) { | |
| 73 | - } | |
| 55 | + async fn on_inbox_abuse(&self, _domain: &str, _actor_uri: Option<&str>, _reason: &str) {} | |
| 74 | 56 | |
| 75 | 57 | /// An AP-native auth challenge was initiated by a remote IdP. |
| 76 | 58 | /// `handle` is the requested `@user@domain`. |
@@ -112,13 +94,7 @@ pub trait FediverseEventObserver: Send + Sync {
| 112 | 94 | /// `fediverse_resource_shares.remote_identity_id`, |
| 113 | 95 | /// `team_members.remote_identity_id`, and any consumer-side |
| 114 | 96 | /// tables such as `ssh_keys.remote_identity_id`). |
| 115 | - async fn on_delete_actor( | |
| 116 | - &self, | |
| 117 | - _domain: &str, | |
| 118 | - _ap_id: &str, | |
| 119 | - _remote_identity_id: Uuid, | |
| 120 | - ) { | |
| 121 | - } | |
| 97 | + async fn on_delete_actor(&self, _domain: &str, _ap_id: &str, _remote_identity_id: Uuid) {} | |
| 122 | 98 | } |
| 123 | 99 | |
| 124 | 100 | /// No-op observer. Wired as the default extension when no consumer |
@@ -133,8 +109,8 @@ impl FediverseEventObserver for NoopFediverseObserver {}
| 133 | 109 | #[cfg(test)] |
| 134 | 110 | mod tests { |
| 135 | 111 | use super::*; |
| 136 | - use std::sync::atomic::{AtomicUsize, Ordering}; | |
| 137 | 112 | use std::sync::Arc; |
| 113 | + use std::sync::atomic::{AtomicUsize, Ordering}; | |
| 138 | 114 | |
| 139 | 115 | struct Counting { |
| 140 | 116 | sig_calls: AtomicUsize, |
@@ -167,7 +143,8 @@ mod tests {
| 167 | 143 | async fn noop_observer_swallows_every_call() { |
| 168 | 144 | let obs: Arc<dyn FediverseEventObserver> = Arc::new(NoopFediverseObserver); |
| 169 | 145 | obs.on_signature_failure("x.tld", None, "bad sig").await; |
| 170 | - obs.on_replay_attempt("x.tld", None, serde_json::json!({})).await; | |
| 146 | + obs.on_replay_attempt("x.tld", None, serde_json::json!({})) | |
| 147 | + .await; | |
| 171 | 148 | obs.on_fetch_ssrf("evil.local", Some("127.0.0.1")).await; |
| 172 | 149 | obs.on_inbox_abuse("x.tld", None, "oversize").await; |
| 173 | 150 | obs.on_auth_challenge("x.tld", "@alice@a.tld").await; |
M
crates/rustwarden-fediverse/src/routes.rs
+1
-1
@@ -21,8 +21,8 @@ use sea_orm::DatabaseConnection;
| 21 | 21 | const INBOX_BODY_LIMIT_BYTES: usize = 1024 * 1024; |
| 22 | 22 | |
| 23 | 23 | use crate::config::FediverseConfig; |
| 24 | -use rustwarden_core::config::auth::AuthConfig; | |
| 25 | 24 | use crate::services::fediverse_signal_service::{AlwaysAcceptVerifier, SignatureVerifier}; |
| 25 | +use rustwarden_core::config::auth::AuthConfig; | |
| 26 | 26 | |
| 27 | 27 | use super::actor; |
| 28 | 28 | use super::auth_routes::{self, AuthFlowState}; |
M
crates/rustwarden-fediverse/src/services/actor_key_service.rs
+13
-13
@@ -100,9 +100,10 @@ impl ActorKeyService {
| 100 | 100 | .await? |
| 101 | 101 | .ok_or_else(|| AppError::NotFound(format!("User {user_id} not found")))?; |
| 102 | 102 | |
| 103 | - if let (Some(public_pem), Some(encrypted_private)) = | |
| 104 | - (model.ap_public_key_pem.clone(), model.ap_private_key_pem.clone()) | |
| 105 | - { | |
| 103 | + if let (Some(public_pem), Some(encrypted_private)) = ( | |
| 104 | + model.ap_public_key_pem.clone(), | |
| 105 | + model.ap_private_key_pem.clone(), | |
| 106 | + ) { | |
| 106 | 107 | let private_pem = CryptoService::decrypt(&encrypted_private)?; |
| 107 | 108 | return Ok(ActorKeyMaterial { |
| 108 | 109 | public_pem, |
@@ -144,11 +145,7 @@ mod tests {
| 144 | 145 | /// the env var does not race with another test clearing it. |
| 145 | 146 | static ENV_LOCK: Mutex<()> = Mutex::new(()); |
| 146 | 147 | |
| 147 | - fn user_row( | |
| 148 | - id: Uuid, | |
| 149 | - public: Option<String>, | |
| 150 | - private: Option<String>, | |
| 151 | - ) -> user::Model { | |
| 148 | + fn user_row(id: Uuid, public: Option<String>, private: Option<String>) -> user::Model { | |
| 152 | 149 | let now = Utc::now(); |
| 153 | 150 | user::Model { |
| 154 | 151 | id, |
@@ -281,7 +278,8 @@ mod tests {
| 281 | 278 | set_test_encryption_key(); |
| 282 | 279 | let user_id = Uuid::new_v4(); |
| 283 | 280 | let public_pem = "-----BEGIN PUBLIC KEY-----\nstored\n-----END PUBLIC KEY-----\n"; |
| 284 | - let plaintext_priv = "-----BEGIN PRIVATE KEY-----\nstored-priv\n-----END PRIVATE KEY-----\n"; | |
| 281 | + let plaintext_priv = | |
| 282 | + "-----BEGIN PRIVATE KEY-----\nstored-priv\n-----END PRIVATE KEY-----\n"; | |
| 285 | 283 | let encrypted_priv = CryptoService::encrypt(plaintext_priv).expect("encrypt"); |
| 286 | 284 | |
| 287 | 285 | let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres) |
@@ -340,9 +338,7 @@ mod tests {
| 340 | 338 | }]) |
| 341 | 339 | .into_connection(); |
| 342 | 340 | |
| 343 | - let material = ActorKeyService::ensure_keys(&db, user_id) | |
| 344 | - .await | |
| 345 | - .unwrap(); | |
| 341 | + let material = ActorKeyService::ensure_keys(&db, user_id).await.unwrap(); | |
| 346 | 342 | // Sanity: round-trip via CryptoService confirms the private PEM |
| 347 | 343 | // returned to the caller is decrypted, while the value the |
| 348 | 344 | // service handed to the DB layer was `enc:`-prefixed. |
@@ -367,7 +363,11 @@ mod tests {
| 367 | 363 | .into_connection(); |
| 368 | 364 | |
| 369 | 365 | let material = ActorKeyService::ensure_keys(&db, user_id).await.unwrap(); |
| 370 | - assert!(material.private_pem.starts_with("-----BEGIN PRIVATE KEY-----")); | |
| 366 | + assert!( | |
| 367 | + material | |
| 368 | + .private_pem | |
| 369 | + .starts_with("-----BEGIN PRIVATE KEY-----") | |
| 370 | + ); | |
| 371 | 371 | assert!(material.private_pem.contains("-----END PRIVATE KEY-----")); |
| 372 | 372 | } |
| 373 | 373 |
M
crates/rustwarden-fediverse/src/services/fediverse_auth_service.rs
+20
-23
@@ -19,10 +19,10 @@ use std::time::Duration;
| 19 | 19 | use uuid::Uuid; |
| 20 | 20 | |
| 21 | 21 | use crate::config::FediverseConfig; |
| 22 | +use crate::models::fediverse_auth_challenge; | |
| 22 | 23 | use rustwarden_core::auth::jwt::JwtService; |
| 23 | 24 | use rustwarden_core::config::auth::AuthConfig; |
| 24 | 25 | use rustwarden_core::error::AppError; |
| 25 | -use crate::models::fediverse_auth_challenge; | |
| 26 | 26 | |
| 27 | 27 | /// State machine for [`fediverse_auth_challenge::Model::status`]. |
| 28 | 28 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
@@ -126,9 +126,10 @@ impl FediverseAuthService {
| 126 | 126 | ) -> Result<InitiatedChallenge, AppError> { |
| 127 | 127 | let domain = handle_domain(&request.handle)?; |
| 128 | 128 | let now = Utc::now(); |
| 129 | - let expires_at = now + ChronoDuration::from_std(ttl).map_err(|e| { | |
| 130 | - AppError::Internal(format!("ttl out of range for chrono::Duration: {e}")) | |
| 131 | - })?; | |
| 129 | + let expires_at = now | |
| 130 | + + ChronoDuration::from_std(ttl).map_err(|e| { | |
| 131 | + AppError::Internal(format!("ttl out of range for chrono::Duration: {e}")) | |
| 132 | + })?; | |
| 132 | 133 | let nonce = generate_nonce(); |
| 133 | 134 | |
| 134 | 135 | let id = Uuid::new_v4(); |
@@ -371,7 +372,7 @@ impl FediverseAuthService {
| 371 | 372 | other => { |
| 372 | 373 | return Err(AppError::Validation(format!( |
| 373 | 374 | "record_inbound_decision: unsupported type `{other}` (expected Accept|Reject)" |
| 374 | - ))) | |
| 375 | + ))); | |
| 375 | 376 | } |
| 376 | 377 | }; |
| 377 | 378 |
@@ -395,9 +396,7 @@ impl FediverseAuthService {
| 395 | 396 | AppError::NotFound(format!("Auth challenge {challenge_id} not found")) |
| 396 | 397 | })?; |
| 397 | 398 | |
| 398 | - if model.status == ChallengeStatus::Pending.as_str() | |
| 399 | - && model.expires_at < Utc::now() | |
| 400 | - { | |
| 399 | + if model.status == ChallengeStatus::Pending.as_str() && model.expires_at < Utc::now() { | |
| 401 | 400 | let mut active: fediverse_auth_challenge::ActiveModel = model.into(); |
| 402 | 401 | active.status = Set(ChallengeStatus::Expired.as_str().to_string()); |
| 403 | 402 | return Ok(active.update(db).await?); |
@@ -436,8 +435,7 @@ impl FediverseAuthService {
| 436 | 435 | |
| 437 | 436 | let remote_identity_id = model.requested_remote_id.ok_or_else(|| { |
| 438 | 437 | AppError::Internal( |
| 439 | - "Approved challenge missing remote_identity_id (data integrity bug)" | |
| 440 | - .to_string(), | |
| 438 | + "Approved challenge missing remote_identity_id (data integrity bug)".to_string(), | |
| 441 | 439 | ) |
| 442 | 440 | })?; |
| 443 | 441 | let handle = model.handle.clone(); |
@@ -555,9 +553,7 @@ fn extract_nonce_from_decision_object(body: &serde_json::Value) -> Result<String
| 555 | 553 | fn handle_domain(handle: &str) -> Result<String, AppError> { |
| 556 | 554 | let trimmed = handle.trim().trim_start_matches('@'); |
| 557 | 555 | let (_, domain) = trimmed.split_once('@').ok_or_else(|| { |
| 558 | - AppError::Validation(format!( | |
| 559 | - "Invalid handle '{handle}': expected user@domain", | |
| 560 | - )) | |
| 556 | + AppError::Validation(format!("Invalid handle '{handle}': expected user@domain",)) | |
| 561 | 557 | })?; |
| 562 | 558 | if domain.is_empty() { |
| 563 | 559 | return Err(AppError::Validation(format!( |
@@ -694,7 +690,8 @@ mod tests {
| 694 | 690 | let b = generate_nonce(); |
| 695 | 691 | assert_ne!(a, b); |
| 696 | 692 | assert!( |
| 697 | - a.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_'), | |
| 693 | + a.bytes() | |
| 694 | + .all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_'), | |
| 698 | 695 | "nonce must be URL-safe base64, got {a}", |
| 699 | 696 | ); |
| 700 | 697 | assert!(a.len() >= 32, "nonce length suspicious: {}", a.len()); |
@@ -745,10 +742,9 @@ mod tests {
| 745 | 742 | }]) |
| 746 | 743 | .into_connection(); |
| 747 | 744 | |
| 748 | - let updated = | |
| 749 | - FediverseAuthService::record_decision(&db, id, ChallengeDecision::Reject) | |
| 750 | - .await | |
| 751 | - .unwrap(); | |
| 745 | + let updated = FediverseAuthService::record_decision(&db, id, ChallengeDecision::Reject) | |
| 746 | + .await | |
| 747 | + .unwrap(); | |
| 752 | 748 | assert_eq!(updated.status, "rejected"); |
| 753 | 749 | } |
| 754 | 750 |
@@ -838,7 +834,10 @@ mod tests {
| 838 | 834 | |
| 839 | 835 | // ─── Phase 2 (RFC 0006) — inbox enrichment ────────────────────── |
| 840 | 836 | |
| 841 | - fn fresh_remote_identity(actor_url: &str, domain: &str) -> crate::models::remote_identity::Model { | |
| 837 | + fn fresh_remote_identity( | |
| 838 | + actor_url: &str, | |
| 839 | + domain: &str, | |
| 840 | + ) -> crate::models::remote_identity::Model { | |
| 842 | 841 | let now = Utc::now(); |
| 843 | 842 | crate::models::remote_identity::Model { |
| 844 | 843 | id: Uuid::new_v4(), |
@@ -904,8 +903,7 @@ mod tests {
| 904 | 903 | |
| 905 | 904 | #[tokio::test] |
| 906 | 905 | async fn persist_inbound_request_rejects_wrong_idp_domain() { |
| 907 | - let db: DatabaseConnection = | |
| 908 | - MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 906 | + let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 909 | 907 | let body = serde_json::json!({ |
| 910 | 908 | "actor": "https://b.tld/", |
| 911 | 909 | "target": "@alice@evil.tld", |
@@ -919,8 +917,7 @@ mod tests {
| 919 | 917 | |
| 920 | 918 | #[tokio::test] |
| 921 | 919 | async fn persist_inbound_request_rejects_missing_actor() { |
| 922 | - let db: DatabaseConnection = | |
| 923 | - MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 920 | + let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 924 | 921 | let body = serde_json::json!({ |
| 925 | 922 | "target": "@alice@a.tld", |
| 926 | 923 | "nonce": "n", |
M
crates/rustwarden-fediverse/src/services/fediverse_invite_service.rs
+42
-29
@@ -9,15 +9,13 @@
| 9 | 9 | //! |
| 10 | 10 | |
| 11 | 11 | use chrono::{Duration as ChronoDuration, Utc}; |
| 12 | -use sea_orm::{ | |
| 13 | - ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set, | |
| 14 | -}; | |
| 12 | +use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set}; | |
| 15 | 13 | use std::time::Duration; |
| 16 | 14 | use uuid::Uuid; |
| 17 | 15 | |
| 18 | 16 | use crate::config::FediverseConfig; |
| 19 | -use rustwarden_core::error::AppError; | |
| 20 | 17 | use crate::models::fediverse_pending_invite; |
| 18 | +use rustwarden_core::error::AppError; | |
| 21 | 19 | use rustwarden_core::models::{team, team_member}; |
| 22 | 20 | use rustwarden_core::services::team_service::TeamRole; |
| 23 | 21 |
@@ -77,9 +75,10 @@ impl FediverseInviteService {
| 77 | 75 | ttl: Duration, |
| 78 | 76 | ) -> Result<fediverse_pending_invite::Model, AppError> { |
| 79 | 77 | let now = Utc::now(); |
| 80 | - let expires_at = now + ChronoDuration::from_std(ttl).map_err(|e| { | |
| 81 | - AppError::Internal(format!("ttl out of range for chrono::Duration: {e}")) | |
| 82 | - })?; | |
| 78 | + let expires_at = now | |
| 79 | + + ChronoDuration::from_std(ttl).map_err(|e| { | |
| 80 | + AppError::Internal(format!("ttl out of range for chrono::Duration: {e}")) | |
| 81 | + })?; | |
| 83 | 82 | let id = Uuid::new_v4(); |
| 84 | 83 | let active = fediverse_pending_invite::ActiveModel { |
| 85 | 84 | id: Set(id), |
@@ -108,9 +107,7 @@ impl FediverseInviteService {
| 108 | 107 | let invite = fediverse_pending_invite::Entity::find_by_id(invite_id) |
| 109 | 108 | .one(db) |
| 110 | 109 | .await? |
| 111 | - .ok_or_else(|| { | |
| 112 | - AppError::NotFound(format!("Invite {invite_id} not found")) | |
| 113 | - })?; | |
| 110 | + .ok_or_else(|| AppError::NotFound(format!("Invite {invite_id} not found")))?; | |
| 114 | 111 | |
| 115 | 112 | if invite.status != InviteStatus::Pending.as_str() { |
| 116 | 113 | return Err(AppError::Validation(format!( |
@@ -148,7 +145,13 @@ impl FediverseInviteService {
| 148 | 145 | db: &DatabaseConnection, |
| 149 | 146 | invite_id: Uuid, |
| 150 | 147 | ) -> Result<fediverse_pending_invite::Model, AppError> { |
| 151 | - Self::transition_terminal(db, invite_id, InviteStatus::Rejected, &[InviteStatus::Pending]).await | |
| 148 | + Self::transition_terminal( | |
| 149 | + db, | |
| 150 | + invite_id, | |
| 151 | + InviteStatus::Rejected, | |
| 152 | + &[InviteStatus::Pending], | |
| 153 | + ) | |
| 154 | + .await | |
| 152 | 155 | } |
| 153 | 156 | |
| 154 | 157 | /// Inviter-side revoke. Allowed both before and after acceptance |
@@ -175,10 +178,7 @@ impl FediverseInviteService {
| 175 | 178 | ) -> Result<Vec<fediverse_pending_invite::Model>, AppError> { |
| 176 | 179 | Ok(fediverse_pending_invite::Entity::find() |
| 177 | 180 | .filter(fediverse_pending_invite::Column::TeamId.eq(team_id)) |
| 178 | - .filter( | |
| 179 | - fediverse_pending_invite::Column::Status | |
| 180 | - .eq(InviteStatus::Pending.as_str()), | |
| 181 | - ) | |
| 181 | + .filter(fediverse_pending_invite::Column::Status.eq(InviteStatus::Pending.as_str())) | |
| 182 | 182 | .all(db) |
| 183 | 183 | .await?) |
| 184 | 184 | } |
@@ -194,10 +194,7 @@ impl FediverseInviteService {
| 194 | 194 | ) -> Result<Vec<fediverse_pending_invite::Model>, AppError> { |
| 195 | 195 | Ok(fediverse_pending_invite::Entity::find() |
| 196 | 196 | .filter(fediverse_pending_invite::Column::InvitedHandle.eq(handle)) |
| 197 | - .filter( | |
| 198 | - fediverse_pending_invite::Column::Status | |
| 199 | - .eq(InviteStatus::Pending.as_str()), | |
| 200 | - ) | |
| 197 | + .filter(fediverse_pending_invite::Column::Status.eq(InviteStatus::Pending.as_str())) | |
| 201 | 198 | .all(db) |
| 202 | 199 | .await?) |
| 203 | 200 | } |
@@ -211,9 +208,7 @@ impl FediverseInviteService {
| 211 | 208 | let invite = fediverse_pending_invite::Entity::find_by_id(invite_id) |
| 212 | 209 | .one(db) |
| 213 | 210 | .await? |
| 214 | - .ok_or_else(|| { | |
| 215 | - AppError::NotFound(format!("Invite {invite_id} not found")) | |
| 216 | - })?; | |
| 211 | + .ok_or_else(|| AppError::NotFound(format!("Invite {invite_id} not found")))?; | |
| 217 | 212 | let current = InviteStatus::parse(&invite.status); |
| 218 | 213 | if !from.contains(¤t) { |
| 219 | 214 | return Err(AppError::Validation(format!( |
@@ -359,7 +354,10 @@ mod tests {
| 359 | 354 | assert_eq!(payload["type"], serde_json::json!("Invite")); |
| 360 | 355 | assert_eq!(payload["target"], serde_json::json!("alice@a.tld")); |
| 361 | 356 | assert_eq!(payload["object"]["role"], serde_json::json!("member")); |
| 362 | - assert_eq!(payload["object"]["team"]["name"], serde_json::json!("Engineering")); | |
| 357 | + assert_eq!( | |
| 358 | + payload["object"]["team"]["name"], | |
| 359 | + serde_json::json!("Engineering") | |
| 360 | + ); | |
| 363 | 361 | } |
| 364 | 362 | |
| 365 | 363 | #[test] |
@@ -367,7 +365,10 @@ mod tests {
| 367 | 365 | let config = test_fediverse_config(); |
| 368 | 366 | let payload = build_accept_invite_payload(&config, "https://b.tld/invites/x"); |
| 369 | 367 | assert_eq!(payload["type"], serde_json::json!("Accept")); |
| 370 | - assert_eq!(payload["object"], serde_json::json!("https://b.tld/invites/x")); | |
| 368 | + assert_eq!( | |
| 369 | + payload["object"], | |
| 370 | + serde_json::json!("https://b.tld/invites/x") | |
| 371 | + ); | |
| 371 | 372 | } |
| 372 | 373 | |
| 373 | 374 | #[tokio::test] |
@@ -439,8 +440,14 @@ mod tests {
| 439 | 440 | // 3. INSERT into team_members → returned row |
| 440 | 441 | .append_query_results([vec![new_member.clone()]]) |
| 441 | 442 | .append_exec_results([ |
| 442 | - MockExecResult { last_insert_id: 0, rows_affected: 1 }, | |
| 443 | - MockExecResult { last_insert_id: 0, rows_affected: 1 }, | |
| 443 | + MockExecResult { | |
| 444 | + last_insert_id: 0, | |
| 445 | + rows_affected: 1, | |
| 446 | + }, | |
| 447 | + MockExecResult { | |
| 448 | + last_insert_id: 0, | |
| 449 | + rows_affected: 1, | |
| 450 | + }, | |
| 444 | 451 | ]) |
| 445 | 452 | .into_connection(); |
| 446 | 453 |
@@ -480,7 +487,9 @@ mod tests {
| 480 | 487 | rows_affected: 1, |
| 481 | 488 | }]) |
| 482 | 489 | .into_connection(); |
| 483 | - let updated = FediverseInviteService::reject(&db, invite_id).await.unwrap(); | |
| 490 | + let updated = FediverseInviteService::reject(&db, invite_id) | |
| 491 | + .await | |
| 492 | + .unwrap(); | |
| 484 | 493 | assert_eq!(updated.status, "rejected"); |
| 485 | 494 | } |
| 486 | 495 |
@@ -498,7 +507,9 @@ mod tests {
| 498 | 507 | rows_affected: 1, |
| 499 | 508 | }]) |
| 500 | 509 | .into_connection(); |
| 501 | - let updated = FediverseInviteService::revoke(&db, invite_id).await.unwrap(); | |
| 510 | + let updated = FediverseInviteService::revoke(&db, invite_id) | |
| 511 | + .await | |
| 512 | + .unwrap(); | |
| 502 | 513 | assert_eq!(updated.status, "revoked"); |
| 503 | 514 | } |
| 504 | 515 |
@@ -518,7 +529,9 @@ mod tests {
| 518 | 529 | rows_affected: 1, |
| 519 | 530 | }]) |
| 520 | 531 | .into_connection(); |
| 521 | - let updated = FediverseInviteService::revoke(&db, invite_id).await.unwrap(); | |
| 532 | + let updated = FediverseInviteService::revoke(&db, invite_id) | |
| 533 | + .await | |
| 534 | + .unwrap(); | |
| 522 | 535 | assert_eq!(updated.status, "revoked"); |
| 523 | 536 | } |
| 524 | 537 |
M
crates/rustwarden-fediverse/src/services/fediverse_outbound_service.rs
+41
-55
@@ -16,8 +16,8 @@ use sea_orm::{
| 16 | 16 | use std::time::Duration; |
| 17 | 17 | use uuid::Uuid; |
| 18 | 18 | |
| 19 | -use rustwarden_core::error::AppError; | |
| 20 | 19 | use crate::models::fediverse_outbound_queue; |
| 20 | +use rustwarden_core::error::AppError; | |
| 21 | 21 | |
| 22 | 22 | /// Lifecycle of an outbound queue row. |
| 23 | 23 | /// |
@@ -44,14 +44,7 @@ impl OutboundStatus {
| 44 | 44 | /// `attempts` is the **post-increment** value (number of failed |
| 45 | 45 | /// attempts so far). |
| 46 | 46 | pub fn next_retry_offset(attempts: i32) -> std::time::Duration { |
| 47 | - const LADDER_SECS: &[u64] = &[ | |
| 48 | - 5 * 60, | |
| 49 | - 30 * 60, | |
| 50 | - 2 * 3600, | |
| 51 | - 6 * 3600, | |
| 52 | - 24 * 3600, | |
| 53 | - 48 * 3600, | |
| 54 | - ]; | |
| 47 | + const LADDER_SECS: &[u64] = &[5 * 60, 30 * 60, 2 * 3600, 6 * 3600, 24 * 3600, 48 * 3600]; | |
| 55 | 48 | let bounded = std::cmp::Ord::max(attempts, 1) as usize - 1; |
| 56 | 49 | let idx = bounded.min(LADDER_SECS.len() - 1); |
| 57 | 50 | std::time::Duration::from_secs(LADDER_SECS[idx]) |
@@ -163,9 +156,7 @@ impl FediverseOutboundService {
| 163 | 156 | ) -> Result<Option<fediverse_outbound_queue::Model>, AppError> { |
| 164 | 157 | let now = Utc::now(); |
| 165 | 158 | let candidate = fediverse_outbound_queue::Entity::find() |
| 166 | - .filter( | |
| 167 | - fediverse_outbound_queue::Column::Status.eq(OutboundStatus::Pending.as_str()), | |
| 168 | - ) | |
| 159 | + .filter(fediverse_outbound_queue::Column::Status.eq(OutboundStatus::Pending.as_str())) | |
| 169 | 160 | .filter( |
| 170 | 161 | fediverse_outbound_queue::Column::NextRetryAt |
| 171 | 162 | .is_null() |
@@ -221,42 +212,43 @@ impl FediverseOutboundService {
| 221 | 212 | // crate. Gated by `app_settings.fediverse_observability_enabled` |
| 222 | 213 | // (default `false`) — opt-in to avoid log noise on instances |
| 223 | 214 | // that pipe nothing to an observability backend. |
| 224 | - let observability = | |
| 225 | - is_fediverse_observability_enabled(db).await.unwrap_or(false); | |
| 215 | + let observability = is_fediverse_observability_enabled(db) | |
| 216 | + .await | |
| 217 | + .unwrap_or(false); | |
| 226 | 218 | if observability { |
| 227 | 219 | match (&delivery, updated.status.as_str()) { |
| 228 | - (Ok(()), _) => { | |
| 229 | - tracing::info!( | |
| 230 | - target: "rustwarden::fediverse::outbound", | |
| 231 | - activity_id = %updated.activity_id, | |
| 232 | - target_inbox = %updated.target_inbox, | |
| 233 | - target_domain = %updated.target_domain, | |
| 234 | - attempts = updated.attempts, | |
| 235 | - event = "delivered", | |
| 236 | - ); | |
| 237 | - } | |
| 238 | - (Err(err), status) if status == OutboundStatus::Dead.as_str() => { | |
| 239 | - tracing::error!( | |
| 240 | - target: "rustwarden::fediverse::outbound", | |
| 241 | - activity_id = %updated.activity_id, | |
| 242 | - target_domain = %updated.target_domain, | |
| 243 | - last_error = %err, | |
| 244 | - attempts = updated.attempts, | |
| 245 | - max_attempts = updated.max_attempts, | |
| 246 | - event = "dead", | |
| 247 | - ); | |
| 248 | - } | |
| 249 | - (Err(err), _) => { | |
| 250 | - tracing::warn!( | |
| 251 | - target: "rustwarden::fediverse::outbound", | |
| 252 | - activity_id = %updated.activity_id, | |
| 253 | - target_domain = %updated.target_domain, | |
| 254 | - last_error = %err, | |
| 255 | - attempts = updated.attempts, | |
| 256 | - next_retry_at = ?updated.next_retry_at, | |
| 257 | - event = "deferred", | |
| 258 | - ); | |
| 259 | - } | |
| 220 | + (Ok(()), _) => { | |
| 221 | + tracing::info!( | |
| 222 | + target: "rustwarden::fediverse::outbound", | |
| 223 | + activity_id = %updated.activity_id, | |
| 224 | + target_inbox = %updated.target_inbox, | |
| 225 | + target_domain = %updated.target_domain, | |
| 226 | + attempts = updated.attempts, | |
| 227 | + event = "delivered", | |
| 228 | + ); | |
| 229 | + } | |
| 230 | + (Err(err), status) if status == OutboundStatus::Dead.as_str() => { | |
| 231 | + tracing::error!( | |
| 232 | + target: "rustwarden::fediverse::outbound", | |
| 233 | + activity_id = %updated.activity_id, | |
| 234 | + target_domain = %updated.target_domain, | |
| 235 | + last_error = %err, | |
| 236 | + attempts = updated.attempts, | |
| 237 | + max_attempts = updated.max_attempts, | |
| 238 | + event = "dead", | |
| 239 | + ); | |
| 240 | + } | |
| 241 | + (Err(err), _) => { | |
| 242 | + tracing::warn!( | |
| 243 | + target: "rustwarden::fediverse::outbound", | |
| 244 | + activity_id = %updated.activity_id, | |
| 245 | + target_domain = %updated.target_domain, | |
| 246 | + last_error = %err, | |
| 247 | + attempts = updated.attempts, | |
| 248 | + next_retry_at = ?updated.next_retry_at, | |
| 249 | + event = "deferred", | |
| 250 | + ); | |
| 251 | + } | |
| 260 | 252 | } |
| 261 | 253 | } |
| 262 | 254 |
@@ -269,9 +261,7 @@ impl FediverseOutboundService {
| 269 | 261 | /// structured `tracing::info!` / `warn!` / `error!` emit-points used for |
| 270 | 262 | /// metric aggregation. Cheap (1 SELECT by key) ; cached by the upstream |
| 271 | 263 | /// `AppSettingsService` if the consumer wires its standard cache. |
| 272 | -pub async fn is_fediverse_observability_enabled( | |
| 273 | - db: &DatabaseConnection, | |
| 274 | -) -> Result<bool, AppError> { | |
| 264 | +pub async fn is_fediverse_observability_enabled(db: &DatabaseConnection) -> Result<bool, AppError> { | |
| 275 | 265 | rustwarden_core::services::app_settings_service::AppSettingsService::get_bool( |
| 276 | 266 | db, |
| 277 | 267 | "fediverse_observability_enabled", |
@@ -350,10 +340,7 @@ mod tests {
| 350 | 340 | |
| 351 | 341 | #[async_trait] |
| 352 | 342 | impl OutboundDelivery for ErringDelivery { |
| 353 | - async fn deliver( | |
| 354 | - &self, | |
| 355 | - _item: &fediverse_outbound_queue::Model, | |
| 356 | - ) -> Result<(), AppError> { | |
| 343 | + async fn deliver(&self, _item: &fediverse_outbound_queue::Model) -> Result<(), AppError> { | |
| 357 | 344 | Err(AppError::Internal("simulated transport failure".into())) |
| 358 | 345 | } |
| 359 | 346 | } |
@@ -580,8 +567,7 @@ mod tests {
| 580 | 567 | async fn worker_can_be_built_and_dropped() { |
| 581 | 568 | // Smoke test : building the struct and dropping it before the |
| 582 | 569 | // loop runs must not panic. |
| 583 | - let db: DatabaseConnection = | |
| 584 | - MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 570 | + let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 585 | 571 | let _worker = OutboundWorker::new(db, NoopDelivery, Duration::from_millis(10)); |
| 586 | 572 | // Drop happens at end of scope. |
| 587 | 573 | } |
M
crates/rustwarden-fediverse/src/services/fediverse_signal_service.rs
+38
-65
@@ -12,14 +12,12 @@ use std::sync::Arc;
| 12 | 12 | |
| 13 | 13 | use async_trait::async_trait; |
| 14 | 14 | use chrono::Utc; |
| 15 | -use sea_orm::{ | |
| 16 | - ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set, | |
| 17 | -}; | |
| 15 | +use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set}; | |
| 18 | 16 | use uuid::Uuid; |
| 19 | 17 | |
| 20 | -use rustwarden_core::error::AppError; | |
| 21 | 18 | use crate::models::remote_identity; |
| 22 | 19 | use crate::observer::{FediverseEventObserver, NoopFediverseObserver}; |
| 20 | +use rustwarden_core::error::AppError; | |
| 23 | 21 | use rustwarden_core::models::team_member; |
| 24 | 22 | |
| 25 | 23 | /// Carries the actor metadata extracted from an inbound `Update{Actor}` |
@@ -120,9 +118,8 @@ impl FediverseSignalService {
| 120 | 118 | // for filling in the rest via webfinger lookup before |
| 121 | 119 | // calling apply_update_actor — but if the row truly is new |
| 122 | 120 | // we still want to persist *something* rather than 404. |
| 123 | - let username_domain = parse_handle_from_ap_id(&actor.ap_id).unwrap_or_else(|| { | |
| 124 | - ("unknown".to_string(), "unknown".to_string()) | |
| 125 | - }); | |
| 121 | + let username_domain = parse_handle_from_ap_id(&actor.ap_id) | |
| 122 | + .unwrap_or_else(|| ("unknown".to_string(), "unknown".to_string())); | |
| 126 | 123 | let active = remote_identity::ActiveModel { |
| 127 | 124 | id: Set(Uuid::new_v4()), |
| 128 | 125 | ap_id: Set(actor.ap_id.clone()), |
@@ -264,10 +261,7 @@ impl FediverseSignalService {
| 264 | 261 | ) -> Result<bool, AppError> { |
| 265 | 262 | let result = team_member::Entity::delete_many() |
| 266 | 263 | .filter(team_member::Column::TeamId.eq(team_id)) |
| 267 | - .filter( | |
| 268 | - team_member::Column::RemoteIdentityId | |
| 269 | - .eq(Some(remote_identity_id)), | |
| 270 | - ) | |
| 264 | + .filter(team_member::Column::RemoteIdentityId.eq(Some(remote_identity_id))) | |
| 271 | 265 | .exec(db) |
| 272 | 266 | .await?; |
| 273 | 267 | Ok(result.rows_affected > 0) |
@@ -454,14 +448,13 @@ async fn ssrf_guard_classified(
| 454 | 448 | message: "URL has no host".into(), |
| 455 | 449 | })?; |
| 456 | 450 | let port = url.port_or_known_default().unwrap_or(80); |
| 457 | - let addrs = | |
| 458 | - tokio::net::lookup_host((host, port)) | |
| 459 | - .await | |
| 460 | - .map_err(|e| SsrfRejection { | |
| 461 | - host: host.to_string(), | |
| 462 | - attempted_ip: None, | |
| 463 | - message: format!("DNS resolution failed for {host}: {e}"), | |
| 464 | - })?; | |
| 451 | + let addrs = tokio::net::lookup_host((host, port)) | |
| 452 | + .await | |
| 453 | + .map_err(|e| SsrfRejection { | |
| 454 | + host: host.to_string(), | |
| 455 | + attempted_ip: None, | |
| 456 | + message: format!("DNS resolution failed for {host}: {e}"), | |
| 457 | + })?; | |
| 465 | 458 | for addr in addrs { |
| 466 | 459 | use std::net::IpAddr; |
| 467 | 460 | let ip = addr.ip(); |
@@ -816,10 +809,9 @@ mod tests {
| 816 | 809 | rows_affected: 1, |
| 817 | 810 | }]) |
| 818 | 811 | .into_connection(); |
| 819 | - let outcome = | |
| 820 | - FediverseSignalService::apply_delete_actor(&db, "https://a.tld/users/alice") | |
| 821 | - .await | |
| 822 | - .unwrap(); | |
| 812 | + let outcome = FediverseSignalService::apply_delete_actor(&db, "https://a.tld/users/alice") | |
| 813 | + .await | |
| 814 | + .unwrap(); | |
| 823 | 815 | assert_eq!(outcome.rows_disabled, 1); |
| 824 | 816 | assert_eq!(outcome.jwts_blacklisted, 0); |
| 825 | 817 | } |
@@ -829,10 +821,9 @@ mod tests {
| 829 | 821 | let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres) |
| 830 | 822 | .append_query_results([Vec::<remote_identity::Model>::new()]) |
| 831 | 823 | .into_connection(); |
| 832 | - let outcome = | |
| 833 | - FediverseSignalService::apply_delete_actor(&db, "https://a.tld/users/ghost") | |
| 834 | - .await | |
| 835 | - .unwrap(); | |
| 824 | + let outcome = FediverseSignalService::apply_delete_actor(&db, "https://a.tld/users/ghost") | |
| 825 | + .await | |
| 826 | + .unwrap(); | |
| 836 | 827 | assert_eq!(outcome, DeleteOutcome::default()); |
| 837 | 828 | } |
| 838 | 829 |
@@ -844,10 +835,9 @@ mod tests {
| 844 | 835 | let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres) |
| 845 | 836 | .append_query_results([vec![row]]) |
| 846 | 837 | .into_connection(); |
| 847 | - let outcome = | |
| 848 | - FediverseSignalService::apply_delete_actor(&db, "https://a.tld/users/alice") | |
| 849 | - .await | |
| 850 | - .unwrap(); | |
| 838 | + let outcome = FediverseSignalService::apply_delete_actor(&db, "https://a.tld/users/alice") | |
| 839 | + .await | |
| 840 | + .unwrap(); | |
| 851 | 841 | assert_eq!(outcome.rows_disabled, 0); |
| 852 | 842 | } |
| 853 | 843 |
@@ -860,16 +850,12 @@ mod tests {
| 860 | 850 | |
| 861 | 851 | #[async_trait] |
| 862 | 852 | impl FediverseEventObserver for CapturingDeleteObserver { |
| 863 | - async fn on_delete_actor( | |
| 864 | - &self, | |
| 865 | - domain: &str, | |
| 866 | - ap_id: &str, | |
| 867 | - remote_identity_id: Uuid, | |
| 868 | - ) { | |
| 869 | - self.calls | |
| 870 | - .lock() | |
| 871 | - .unwrap() | |
| 872 | - .push((domain.to_string(), ap_id.to_string(), remote_identity_id)); | |
| 853 | + async fn on_delete_actor(&self, domain: &str, ap_id: &str, remote_identity_id: Uuid) { | |
| 854 | + self.calls.lock().unwrap().push(( | |
| 855 | + domain.to_string(), | |
| 856 | + ap_id.to_string(), | |
| 857 | + remote_identity_id, | |
| 858 | + )); | |
| 873 | 859 | } |
| 874 | 860 | } |
| 875 | 861 |
@@ -952,13 +938,10 @@ mod tests {
| 952 | 938 | rows_affected: 0, |
| 953 | 939 | }]) |
| 954 | 940 | .into_connection(); |
| 955 | - let revoked = FediverseSignalService::apply_revoke_membership( | |
| 956 | - &db, | |
| 957 | - Uuid::new_v4(), | |
| 958 | - Uuid::new_v4(), | |
| 959 | - ) | |
| 960 | - .await | |
| 961 | - .unwrap(); | |
| 941 | + let revoked = | |
| 942 | + FediverseSignalService::apply_revoke_membership(&db, Uuid::new_v4(), Uuid::new_v4()) | |
| 943 | + .await | |
| 944 | + .unwrap(); | |
| 962 | 945 | assert!(!revoked); |
| 963 | 946 | } |
| 964 | 947 |
@@ -970,13 +953,10 @@ mod tests {
| 970 | 953 | rows_affected: 1, |
| 971 | 954 | }]) |
| 972 | 955 | .into_connection(); |
| 973 | - let revoked = FediverseSignalService::apply_revoke_membership( | |
| 974 | - &db, | |
| 975 | - Uuid::new_v4(), | |
| 976 | - Uuid::new_v4(), | |
| 977 | - ) | |
| 978 | - .await | |
| 979 | - .unwrap(); | |
| 956 | + let revoked = | |
| 957 | + FediverseSignalService::apply_revoke_membership(&db, Uuid::new_v4(), Uuid::new_v4()) | |
| 958 | + .await | |
| 959 | + .unwrap(); | |
| 980 | 960 | assert!(revoked); |
| 981 | 961 | } |
| 982 | 962 |
@@ -1387,12 +1367,7 @@ mod tests {
| 1387 | 1367 | async fn on_fetch_ssrf(&self, _host: &str, _ip: Option<&str>) { |
| 1388 | 1368 | self.ssrf.fetch_add(1, Ordering::SeqCst); |
| 1389 | 1369 | } |
| 1390 | - async fn on_inbox_abuse( | |
| 1391 | - &self, | |
| 1392 | - _domain: &str, | |
| 1393 | - _actor: Option<&str>, | |
| 1394 | - reason: &str, | |
| 1395 | - ) { | |
| 1370 | + async fn on_inbox_abuse(&self, _domain: &str, _actor: Option<&str>, reason: &str) { | |
| 1396 | 1371 | self.inbox_abuse_reason |
| 1397 | 1372 | .lock() |
| 1398 | 1373 | .unwrap() |
@@ -1405,8 +1380,7 @@ mod tests {
| 1405 | 1380 | use wiremock::MockServer; |
| 1406 | 1381 | let server = MockServer::start().await; |
| 1407 | 1382 | let actor_url = format!("{}/users/anything", server.uri()); |
| 1408 | - let db: DatabaseConnection = | |
| 1409 | - MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 1383 | + let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 1410 | 1384 | let observer = CountingObserver::default(); |
| 1411 | 1385 | let err = fetch_and_cache_remote_actor_observed( |
| 1412 | 1386 | &db, |
@@ -1436,8 +1410,7 @@ mod tests {
| 1436 | 1410 | .respond_with(ResponseTemplate::new(200).set_body_json(doc)) |
| 1437 | 1411 | .mount(&server) |
| 1438 | 1412 | .await; |
| 1439 | - let db: DatabaseConnection = | |
| 1440 | - MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 1413 | + let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); | |
| 1441 | 1414 | let observer = CountingObserver::default(); |
| 1442 | 1415 | let err = fetch_and_cache_remote_actor_observed( |
| 1443 | 1416 | &db, |
M
crates/rustwarden-fediverse/src/services/fediverse_trust_service.rs
+12
-4
@@ -11,8 +11,8 @@ use chrono::Utc;
| 11 | 11 | use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set}; |
| 12 | 12 | use uuid::Uuid; |
| 13 | 13 | |
| 14 | -use rustwarden_core::error::AppError; | |
| 15 | 14 | use crate::models::{fediverse_blocked_domain, fediverse_trusted_domain}; |
| 15 | +use rustwarden_core::error::AppError; | |
| 16 | 16 | use rustwarden_core::services::app_settings_service::AppSettingsService; |
| 17 | 17 | |
| 18 | 18 | /// Available federation trust modes (`app_settings.fediverse_trust_mode`). |
@@ -116,7 +116,9 @@ impl FediverseTrustService {
| 116 | 116 | /// [`TrustMode::Allowlist`]). |
| 117 | 117 | pub async fn trust_mode(&self) -> Result<TrustMode, AppError> { |
| 118 | 118 | let raw = AppSettingsService::get_setting(&self.db, "fediverse_trust_mode").await?; |
| 119 | - Ok(raw.map(|m| TrustMode::parse(&m.value)).unwrap_or(TrustMode::Allowlist)) | |
| 119 | + Ok(raw | |
| 120 | + .map(|m| TrustMode::parse(&m.value)) | |
| 121 | + .unwrap_or(TrustMode::Allowlist)) | |
| 120 | 122 | } |
| 121 | 123 | |
| 122 | 124 | /// Read `app_settings.fediverse_interop_scope` (default |
@@ -357,7 +359,10 @@ mod tests {
| 357 | 359 | fn block_severity_parses_known_values() { |
| 358 | 360 | assert_eq!(BlockSeverity::parse("suspend"), BlockSeverity::Suspend); |
| 359 | 361 | assert_eq!(BlockSeverity::parse("silence"), BlockSeverity::Silence); |
| 360 | - assert_eq!(BlockSeverity::parse("reject_auth"), BlockSeverity::RejectAuth); | |
| 362 | + assert_eq!( | |
| 363 | + BlockSeverity::parse("reject_auth"), | |
| 364 | + BlockSeverity::RejectAuth | |
| 365 | + ); | |
| 361 | 366 | } |
| 362 | 367 | |
| 363 | 368 | #[test] |
@@ -369,7 +374,10 @@ mod tests {
| 369 | 374 | fn normalize_domain_lowercases_and_strips_www() { |
| 370 | 375 | assert_eq!(normalize_domain("Example.ORG"), "example.org"); |
| 371 | 376 | assert_eq!(normalize_domain("www.example.org"), "example.org"); |
| 372 | - assert_eq!(normalize_domain(" www.Mastodon.SOCIAL "), "mastodon.social"); | |
| 377 | + assert_eq!( | |
| 378 | + normalize_domain(" www.Mastodon.SOCIAL "), | |
| 379 | + "mastodon.social" | |
| 380 | + ); | |
| 373 | 381 | assert_eq!(normalize_domain("example.org"), "example.org"); |
| 374 | 382 | } |
| 375 | 383 |
M
crates/rustwarden-fediverse/src/services/remote_identity_service.rs
+11
-16
@@ -13,11 +13,11 @@ use sea_orm::DatabaseConnection;
| 13 | 13 | use uuid::Uuid; |
| 14 | 14 | |
| 15 | 15 | use crate::config::FediverseConfig; |
| 16 | -use rustwarden_core::error::AppError; | |
| 17 | 16 | use crate::error::FediverseError; |
| 18 | 17 | use crate::fetcher::RemoteFetcher; |
| 19 | -use crate::types::PersonJson; | |
| 20 | 18 | use crate::models::remote_identity; |
| 19 | +use crate::types::PersonJson; | |
| 20 | +use rustwarden_core::error::AppError; | |
| 21 | 21 | |
| 22 | 22 | /// Service orchestrating remote actor resolution and caching. |
| 23 | 23 | /// |
@@ -204,14 +204,8 @@ impl RemoteIdentityService {
| 204 | 204 | |
| 205 | 205 | let now = Utc::now(); |
| 206 | 206 | remote_identity::Entity::update_many() |
| 207 | - .col_expr( | |
| 208 | - remote_identity::Column::DisabledAt, | |
| 209 | - Expr::value(now), | |
| 210 | - ) | |
| 211 | - .col_expr( | |
| 212 | - remote_identity::Column::UpdatedAt, | |
| 213 | - Expr::value(now), | |
| 214 | - ) | |
| 207 | + .col_expr(remote_identity::Column::DisabledAt, Expr::value(now)) | |
| 208 | + .col_expr(remote_identity::Column::UpdatedAt, Expr::value(now)) | |
| 215 | 209 | .filter(remote_identity::Column::Id.eq(id)) |
| 216 | 210 | .exec(&self.db) |
| 217 | 211 | .await?; |
@@ -329,7 +323,10 @@ mod tests {
| 329 | 323 | .append_query_results([Vec::<remote_identity::Model>::new()]) |
| 330 | 324 | .into_connection(); |
| 331 | 325 | let svc = make_service(db); |
| 332 | - let result = svc.find_by_uri("https://other.tld/users/ghost").await.unwrap(); | |
| 326 | + let result = svc | |
| 327 | + .find_by_uri("https://other.tld/users/ghost") | |
| 328 | + .await | |
| 329 | + .unwrap(); | |
| 333 | 330 | assert!(result.is_none()); |
| 334 | 331 | } |
| 335 | 332 |
@@ -392,10 +389,7 @@ mod tests {
| 392 | 389 | .upsert_from_person(person, "expected.tld") |
| 393 | 390 | .await |
| 394 | 391 | .expect_err("must fail"); |
| 395 | - assert!( | |
| 396 | - matches!(err, FediverseError::DomainMismatch), | |
| 397 | - "got {err:?}" | |
| 398 | - ); | |
| 392 | + assert!(matches!(err, FediverseError::DomainMismatch), "got {err:?}"); | |
| 399 | 393 | } |
| 400 | 394 | |
| 401 | 395 | // ─── Test 8 — mark_disabled ────────────────────────────────────── |
@@ -424,7 +418,8 @@ mod tests {
| 424 | 418 | .into_connection(); |
| 425 | 419 | // UnconfiguredFetcher would return Err if invoked — fresh cache must |
| 426 | 420 | // not invoke it. |
| 427 | - let svc = RemoteIdentityService::new(db, Arc::new(UnconfiguredFetcher), &config_for_tests()); | |
| 421 | + let svc = | |
| 422 | + RemoteIdentityService::new(db, Arc::new(UnconfiguredFetcher), &config_for_tests()); | |
| 428 | 423 | let resolved = svc |
| 429 | 424 | .resolve_handle("@alice@other.tld") |
| 430 | 425 | .await |
M
crates/rustwarden-fediverse/src/services/remote_personal_access_token_service.rs
+10
-18
@@ -217,10 +217,7 @@ impl<S: TokenScope> RemotePersonalAccessTokenService<S> {
| 217 | 217 | /// # Errors |
| 218 | 218 | /// |
| 219 | 219 | /// - [`AppError::Database`] on persistence error. |
| 220 | - pub async fn revoke( | |
| 221 | - db: &DatabaseConnection, | |
| 222 | - token_id: Uuid, | |
| 223 | - ) -> Result<bool, AppError> { | |
| 220 | + pub async fn revoke(db: &DatabaseConnection, token_id: Uuid) -> Result<bool, AppError> { | |
| 224 | 221 | let Some(row) = fediverse_personal_access_token::Entity::find_by_id(token_id) |
| 225 | 222 | .one(db) |
| 226 | 223 | .await? |
@@ -246,8 +243,7 @@ impl<S: TokenScope> RemotePersonalAccessTokenService<S> {
| 246 | 243 | ) -> Result<Vec<fediverse_personal_access_token::Model>, AppError> { |
| 247 | 244 | let rows = fediverse_personal_access_token::Entity::find() |
| 248 | 245 | .filter( |
| 249 | - fediverse_personal_access_token::Column::RemoteIdentityId | |
| 250 | - .eq(remote_identity_id), | |
| 246 | + fediverse_personal_access_token::Column::RemoteIdentityId.eq(remote_identity_id), | |
| 251 | 247 | ) |
| 252 | 248 | .filter(fediverse_personal_access_token::Column::RevokedAt.is_null()) |
| 253 | 249 | .order_by_desc(fediverse_personal_access_token::Column::CreatedAt) |
@@ -270,8 +266,7 @@ impl<S: TokenScope> RemotePersonalAccessTokenService<S> {
| 270 | 266 | sea_orm::sea_query::Expr::value(now), |
| 271 | 267 | ) |
| 272 | 268 | .filter( |
| 273 | - fediverse_personal_access_token::Column::RemoteIdentityId | |
| 274 | - .eq(remote_identity_id), | |
| 269 | + fediverse_personal_access_token::Column::RemoteIdentityId.eq(remote_identity_id), | |
| 275 | 270 | ) |
| 276 | 271 | .filter(fediverse_personal_access_token::Column::RevokedAt.is_null()) |
| 277 | 272 | .exec(db) |
@@ -479,11 +474,9 @@ mod tests {
| 479 | 474 | #[tokio::test] |
| 480 | 475 | async fn verify_rejects_missing_prefix() { |
| 481 | 476 | let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); |
| 482 | - let result = RemotePersonalAccessTokenService::<TestScope>::verify( | |
| 483 | - &db, "no_prefix_token", | |
| 484 | - ) | |
| 485 | - .await | |
| 486 | - .unwrap(); | |
| 477 | + let result = RemotePersonalAccessTokenService::<TestScope>::verify(&db, "no_prefix_token") | |
| 478 | + .await | |
| 479 | + .unwrap(); | |
| 487 | 480 | assert!(result.is_none()); |
| 488 | 481 | } |
| 489 | 482 |
@@ -572,11 +565,10 @@ mod tests {
| 572 | 565 | .append_query_results([vec![identity]]) |
| 573 | 566 | .append_query_results([vec![updated]]) |
| 574 | 567 | .into_connection(); |
| 575 | - let (model, scopes) = | |
| 576 | - RemotePersonalAccessTokenService::<TestScope>::verify(&db, &plain) | |
| 577 | - .await | |
| 578 | - .unwrap() | |
| 579 | - .expect("verify should yield a valid match"); | |
| 568 | + let (model, scopes) = RemotePersonalAccessTokenService::<TestScope>::verify(&db, &plain) | |
| 569 | + .await | |
| 570 | + .unwrap() | |
| 571 | + .expect("verify should yield a valid match"); | |
| 580 | 572 | assert_eq!(scopes, vec![TestScope::Read, TestScope::Write]); |
| 581 | 573 | assert_eq!(model.token_hash, hash); |
| 582 | 574 | } |
M
crates/rustwarden-fediverse/src/services/remote_resource_share_service.rs
+24
-45
@@ -79,9 +79,7 @@ impl RemoteResourceShareService {
| 79 | 79 | let entry = ResourceService::find_by_type_and_id(db, resource_type, resource_id) |
| 80 | 80 | .await? |
| 81 | 81 | .ok_or_else(|| { |
| 82 | - AppError::NotFound(format!( | |
| 83 | - "Resource not found: {resource_type}/{resource_id}" | |
| 84 | - )) | |
| 82 | + AppError::NotFound(format!("Resource not found: {resource_type}/{resource_id}")) | |
| 85 | 83 | })?; |
| 86 | 84 | |
| 87 | 85 | if !PERMISSION_HIERARCHY.contains(&permission_level) { |
@@ -93,9 +91,7 @@ impl RemoteResourceShareService {
| 93 | 91 | // Upsert by (resource_id, remote_identity_id). |
| 94 | 92 | let existing = fediverse_resource_share::Entity::find() |
| 95 | 93 | .filter(fediverse_resource_share::Column::ResourceId.eq(entry.id)) |
| 96 | - .filter( | |
| 97 | - fediverse_resource_share::Column::RemoteIdentityId.eq(remote_identity_id), | |
| 98 | - ) | |
| 94 | + .filter(fediverse_resource_share::Column::RemoteIdentityId.eq(remote_identity_id)) | |
| 99 | 95 | .one(db) |
| 100 | 96 | .await?; |
| 101 | 97 |
@@ -139,16 +135,12 @@ impl RemoteResourceShareService {
| 139 | 135 | let entry = ResourceService::find_by_type_and_id(db, resource_type, resource_id) |
| 140 | 136 | .await? |
| 141 | 137 | .ok_or_else(|| { |
| 142 | - AppError::NotFound(format!( | |
| 143 | - "Resource not found: {resource_type}/{resource_id}" | |
| 144 | - )) | |
| 138 | + AppError::NotFound(format!("Resource not found: {resource_type}/{resource_id}")) | |
| 145 | 139 | })?; |
| 146 | 140 | |
| 147 | 141 | let result = fediverse_resource_share::Entity::delete_many() |
| 148 | 142 | .filter(fediverse_resource_share::Column::ResourceId.eq(entry.id)) |
| 149 | - .filter( | |
| 150 | - fediverse_resource_share::Column::RemoteIdentityId.eq(remote_identity_id), | |
| 151 | - ) | |
| 143 | + .filter(fediverse_resource_share::Column::RemoteIdentityId.eq(remote_identity_id)) | |
| 152 | 144 | .exec(db) |
| 153 | 145 | .await?; |
| 154 | 146 | Ok(result.rows_affected > 0) |
@@ -175,16 +167,12 @@ impl RemoteResourceShareService {
| 175 | 167 | let entry = ResourceService::find_by_type_and_id(db, resource_type, resource_id) |
| 176 | 168 | .await? |
| 177 | 169 | .ok_or_else(|| { |
| 178 | - AppError::NotFound(format!( | |
| 179 | - "Resource not found: {resource_type}/{resource_id}" | |
| 180 | - )) | |
| 170 | + AppError::NotFound(format!("Resource not found: {resource_type}/{resource_id}")) | |
| 181 | 171 | })?; |
| 182 | 172 | |
| 183 | 173 | let row = fediverse_resource_share::Entity::find() |
| 184 | 174 | .filter(fediverse_resource_share::Column::ResourceId.eq(entry.id)) |
| 185 | - .filter( | |
| 186 | - fediverse_resource_share::Column::RemoteIdentityId.eq(remote_identity_id), | |
| 187 | - ) | |
| 175 | + .filter(fediverse_resource_share::Column::RemoteIdentityId.eq(remote_identity_id)) | |
| 188 | 176 | .one(db) |
| 189 | 177 | .await?; |
| 190 | 178 | Ok(row.map(|m| m.permission_level)) |
@@ -205,9 +193,7 @@ impl RemoteResourceShareService {
| 205 | 193 | let entry = ResourceService::find_by_type_and_id(db, resource_type, resource_id) |
| 206 | 194 | .await? |
| 207 | 195 | .ok_or_else(|| { |
| 208 | - AppError::NotFound(format!( | |
| 209 | - "Resource not found: {resource_type}/{resource_id}" | |
| 210 | - )) | |
| 196 | + AppError::NotFound(format!("Resource not found: {resource_type}/{resource_id}")) | |
| 211 | 197 | })?; |
| 212 | 198 | |
| 213 | 199 | let rows = fediverse_resource_share::Entity::find() |
@@ -226,9 +212,7 @@ impl RemoteResourceShareService {
| 226 | 212 | remote_identity_id: Uuid, |
| 227 | 213 | ) -> Result<Vec<fediverse_resource_share::Model>, AppError> { |
| 228 | 214 | let rows = fediverse_resource_share::Entity::find() |
| 229 | - .filter( | |
| 230 | - fediverse_resource_share::Column::RemoteIdentityId.eq(remote_identity_id), | |
| 231 | - ) | |
| 215 | + .filter(fediverse_resource_share::Column::RemoteIdentityId.eq(remote_identity_id)) | |
| 232 | 216 | .order_by_desc(fediverse_resource_share::Column::CreatedAt) |
| 233 | 217 | .all(db) |
| 234 | 218 | .await?; |
@@ -253,7 +237,11 @@ mod tests {
| 253 | 237 | } |
| 254 | 238 | } |
| 255 | 239 | |
| 256 | - fn share_row(resource_id: Uuid, remote_id: Uuid, level: &str) -> fediverse_resource_share::Model { | |
| 240 | + fn share_row( | |
| 241 | + resource_id: Uuid, | |
| 242 | + remote_id: Uuid, | |
| 243 | + level: &str, | |
| 244 | + ) -> fediverse_resource_share::Model { | |
| 257 | 245 | fediverse_resource_share::Model { |
| 258 | 246 | id: Uuid::new_v4(), |
| 259 | 247 | resource_id, |
@@ -382,14 +370,10 @@ mod tests {
| 382 | 370 | rows_affected: 1, |
| 383 | 371 | }]) |
| 384 | 372 | .into_connection(); |
| 385 | - let deleted = RemoteResourceShareService::unshare( | |
| 386 | - &db, | |
| 387 | - "repository", | |
| 388 | - Uuid::new_v4(), | |
| 389 | - Uuid::new_v4(), | |
| 390 | - ) | |
| 391 | - .await | |
| 392 | - .unwrap(); | |
| 373 | + let deleted = | |
| 374 | + RemoteResourceShareService::unshare(&db, "repository", Uuid::new_v4(), Uuid::new_v4()) | |
| 375 | + .await | |
| 376 | + .unwrap(); | |
| 393 | 377 | assert!(deleted); |
| 394 | 378 | } |
| 395 | 379 |
@@ -403,14 +387,10 @@ mod tests {
| 403 | 387 | rows_affected: 0, |
| 404 | 388 | }]) |
| 405 | 389 | .into_connection(); |
| 406 | - let deleted = RemoteResourceShareService::unshare( | |
| 407 | - &db, | |
| 408 | - "repository", | |
| 409 | - Uuid::new_v4(), | |
| 410 | - Uuid::new_v4(), | |
| 411 | - ) | |
| 412 | - .await | |
| 413 | - .unwrap(); | |
| 390 | + let deleted = | |
| 391 | + RemoteResourceShareService::unshare(&db, "repository", Uuid::new_v4(), Uuid::new_v4()) | |
| 392 | + .await | |
| 393 | + .unwrap(); | |
| 414 | 394 | assert!(!deleted); |
| 415 | 395 | } |
| 416 | 396 |
@@ -474,10 +454,9 @@ mod tests {
| 474 | 454 | let db = MockDatabase::new(DatabaseBackend::Postgres) |
| 475 | 455 | .append_query_results([Vec::<fediverse_resource_share::Model>::new()]) |
| 476 | 456 | .into_connection(); |
| 477 | - let rows = | |
| 478 | - RemoteResourceShareService::list_for_remote_identity(&db, Uuid::new_v4()) | |
| 479 | - .await | |
| 480 | - .unwrap(); | |
| 457 | + let rows = RemoteResourceShareService::list_for_remote_identity(&db, Uuid::new_v4()) | |
| 458 | + .await | |
| 459 | + .unwrap(); | |
| 481 | 460 | assert!(rows.is_empty()); |
| 482 | 461 | } |
| 483 | 462 | } |
M
crates/rustwarden-fediverse/src/webfinger.rs
+7
-4
@@ -86,9 +86,9 @@ pub async fn handle(
| 86 | 86 | /// Returns [`FediverseError::InvalidResource`] if the prefix is missing or |
| 87 | 87 | /// the `@` separator is absent or any side is empty. |
| 88 | 88 | pub(crate) fn parse_acct_resource(resource: &str) -> Result<(String, String), FediverseError> { |
| 89 | - let acct = resource | |
| 90 | - .strip_prefix("acct:") | |
| 91 | - .ok_or_else(|| FediverseError::InvalidResource(format!("missing acct: prefix in {resource}")))?; | |
| 89 | + let acct = resource.strip_prefix("acct:").ok_or_else(|| { | |
| 90 | + FediverseError::InvalidResource(format!("missing acct: prefix in {resource}")) | |
| 91 | + })?; | |
| 92 | 92 | |
| 93 | 93 | let (user, domain) = acct.split_once('@').ok_or_else(|| { |
| 94 | 94 | FediverseError::InvalidResource(format!("missing @ separator in {resource}")) |
@@ -133,7 +133,10 @@ mod tests {
| 133 | 133 | async fn count_users_total(&self) -> Result<u64, rustwarden_core::error::AppError> { |
| 134 | 134 | Ok(0) |
| 135 | 135 | } |
| 136 | - async fn count_users_active_since(&self, _days: u32) -> Result<u64, rustwarden_core::error::AppError> { | |
| 136 | + async fn count_users_active_since( | |
| 137 | + &self, | |
| 138 | + _days: u32, | |
| 139 | + ) -> Result<u64, rustwarden_core::error::AppError> { | |
| 137 | 140 | Ok(0) |
| 138 | 141 | } |
| 139 | 142 | async fn count_local_posts(&self) -> Result<u64, rustwarden_core::error::AppError> { |
M
crates/rustwarden-fediverse/tests/e2e_fediverse.rs
+77
-27
@@ -11,12 +11,11 @@
| 11 | 11 | //! they can stay in the default test suite without breaking developers who |
| 12 | 12 | //! haven't booted the testbed. |
| 13 | 13 | |
| 14 | - | |
| 15 | 14 | use base64::Engine; |
| 16 | 15 | use reqwest::{Client, StatusCode}; |
| 17 | -use rustwarden_fediverse::http_signatures::{build_signature_input, rsa_sha256_sign}; | |
| 18 | 16 | use rustwarden_core::services::crypto_service::CryptoService; |
| 19 | -use serde_json::{json, Value}; | |
| 17 | +use rustwarden_fediverse::http_signatures::{build_signature_input, rsa_sha256_sign}; | |
| 18 | +use serde_json::{Value, json}; | |
| 20 | 19 | use std::process::Command; |
| 21 | 20 | use uuid::Uuid; |
| 22 | 21 |
@@ -70,10 +69,13 @@ async fn webfinger_resolves_local_user_on_each_instance() {
| 70 | 69 | let jrd: Value = response.json().await.expect("body must be JSON"); |
| 71 | 70 | assert_eq!(jrd["subject"], format!("acct:joanie@{domain}")); |
| 72 | 71 | let links = jrd["links"].as_array().expect("links must be array"); |
| 73 | - let has_self_ap = links.iter().any(|l| { | |
| 74 | - l["rel"] == "self" && l["type"] == "application/activity+json" | |
| 75 | - }); | |
| 76 | - assert!(has_self_ap, "JRD on {base} must expose a self link with AP type"); | |
| 72 | + let has_self_ap = links | |
| 73 | + .iter() | |
| 74 | + .any(|l| l["rel"] == "self" && l["type"] == "application/activity+json"); | |
| 75 | + assert!( | |
| 76 | + has_self_ap, | |
| 77 | + "JRD on {base} must expose a self link with AP type" | |
| 78 | + ); | |
| 77 | 79 | } |
| 78 | 80 | } |
| 79 | 81 |
@@ -156,10 +158,9 @@ async fn nodeinfo_discovery_returns_links() {
| 156 | 158 | let body: Value = response.json().await.expect("body must be JSON"); |
| 157 | 159 | let links = body["links"].as_array().expect("links must be array"); |
| 158 | 160 | assert!( |
| 159 | - links.iter().any(|l| l["rel"] | |
| 160 | - .as_str() | |
| 161 | - .unwrap_or_default() | |
| 162 | - .contains("nodeinfo")), | |
| 161 | + links | |
| 162 | + .iter() | |
| 163 | + .any(|l| l["rel"].as_str().unwrap_or_default().contains("nodeinfo")), | |
| 163 | 164 | "nodeinfo discovery must expose at least one nodeinfo link" |
| 164 | 165 | ); |
| 165 | 166 | } |
@@ -258,7 +259,10 @@ fn signed_headers(
| 258 | 259 | target_url: &str, |
| 259 | 260 | ) -> (String, String, String) { |
| 260 | 261 | let target = url::Url::parse(target_url).expect("target_url must parse"); |
| 261 | - let host = target.host_str().expect("target_url must have host").to_string(); | |
| 262 | + let host = target | |
| 263 | + .host_str() | |
| 264 | + .expect("target_url must have host") | |
| 265 | + .to_string(); | |
| 262 | 266 | let path = target.path().to_string(); |
| 263 | 267 | |
| 264 | 268 | let date = chrono::Utc::now() |
@@ -280,8 +284,8 @@ fn signed_headers(
| 280 | 284 | }) |
| 281 | 285 | .expect("build_signature_input must succeed"); |
| 282 | 286 | |
| 283 | - let sig_bytes = rsa_sha256_sign(private_pem, input.as_bytes()) | |
| 284 | - .expect("rsa_sha256_sign must succeed"); | |
| 287 | + let sig_bytes = | |
| 288 | + rsa_sha256_sign(private_pem, input.as_bytes()).expect("rsa_sha256_sign must succeed"); | |
| 285 | 289 | let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&sig_bytes); |
| 286 | 290 | |
| 287 | 291 | let sig_header = format!( |
@@ -429,7 +433,10 @@ async fn s1_signed_authrequest_yields_pending_challenge_on_alpha() {
| 429 | 433 | "alpha", |
| 430 | 434 | "SELECT COUNT(*) FROM remote_identities WHERE ap_id='http://beta.local/users/joanie'", |
| 431 | 435 | ); |
| 432 | - assert_eq!(ri_count, "1", "alpha must have 1 remote_identity for joanie@beta"); | |
| 436 | + assert_eq!( | |
| 437 | + ri_count, "1", | |
| 438 | + "alpha must have 1 remote_identity for joanie@beta" | |
| 439 | + ); | |
| 433 | 440 | |
| 434 | 441 | // 6. Alpha must have created a pending challenge for that nonce. |
| 435 | 442 | let after: i64 = psql( |
@@ -448,7 +455,10 @@ async fn s1_signed_authrequest_yields_pending_challenge_on_alpha() {
| 448 | 455 | "alpha", |
| 449 | 456 | &format!("SELECT COUNT(*) FROM fediverse_auth_challenges WHERE nonce='{nonce}'"), |
| 450 | 457 | ); |
| 451 | - assert_eq!(nonce_present, "1", "the inserted challenge must carry our nonce"); | |
| 458 | + assert_eq!( | |
| 459 | + nonce_present, "1", | |
| 460 | + "the inserted challenge must carry our nonce" | |
| 461 | + ); | |
| 452 | 462 | } |
| 453 | 463 | |
| 454 | 464 | // ─────────────────────────────────────────────────────────────────────── |
@@ -741,7 +751,11 @@ async fn s2_admin_invite_to_remote_joanie_creates_team_membership() {
| 741 | 751 | // 1. Create a team on beta. Slug must be unique across runs. |
| 742 | 752 | let team_slug = format!( |
| 743 | 753 | "team-s2-{}", |
| 744 | - Uuid::new_v4().to_string().chars().take(8).collect::<String>() | |
| 754 | + Uuid::new_v4() | |
| 755 | + .to_string() | |
| 756 | + .chars() | |
| 757 | + .take(8) | |
| 758 | + .collect::<String>() | |
| 745 | 759 | ); |
| 746 | 760 | psql( |
| 747 | 761 | "beta", |
@@ -809,7 +823,11 @@ async fn s2_invite_reject_flips_status_to_rejected_no_member() {
| 809 | 823 | |
| 810 | 824 | let team_slug = format!( |
| 811 | 825 | "team-s2-rej-{}", |
| 812 | - Uuid::new_v4().to_string().chars().take(8).collect::<String>() | |
| 826 | + Uuid::new_v4() | |
| 827 | + .to_string() | |
| 828 | + .chars() | |
| 829 | + .take(8) | |
| 830 | + .collect::<String>() | |
| 813 | 831 | ); |
| 814 | 832 | psql( |
| 815 | 833 | "beta", |
@@ -873,7 +891,11 @@ async fn s2_invite_revoke_blocks_subsequent_accept() {
| 873 | 891 | |
| 874 | 892 | let team_slug = format!( |
| 875 | 893 | "team-s2-rev-{}", |
| 876 | - Uuid::new_v4().to_string().chars().take(8).collect::<String>() | |
| 894 | + Uuid::new_v4() | |
| 895 | + .to_string() | |
| 896 | + .chars() | |
| 897 | + .take(8) | |
| 898 | + .collect::<String>() | |
| 877 | 899 | ); |
| 878 | 900 | psql( |
| 879 | 901 | "beta", |
@@ -937,7 +959,11 @@ async fn s2_idp_list_returns_pending_invite_for_handle() {
| 937 | 959 | |
| 938 | 960 | let team_slug = format!( |
| 939 | 961 | "team-s2-list-{}", |
| 940 | - Uuid::new_v4().to_string().chars().take(8).collect::<String>() | |
| 962 | + Uuid::new_v4() | |
| 963 | + .to_string() | |
| 964 | + .chars() | |
| 965 | + .take(8) | |
| 966 | + .collect::<String>() | |
| 941 | 967 | ); |
| 942 | 968 | psql( |
| 943 | 969 | "beta", |
@@ -1023,7 +1049,11 @@ async fn s3_signed_protocol_flow_never_writes_trust_list() {
| 1023 | 1049 | ensure_joanie_alpha_in_beta_cache(&client).await; |
| 1024 | 1050 | let team_slug = format!( |
| 1025 | 1051 | "team-s3-trust-{}", |
| 1026 | - Uuid::new_v4().to_string().chars().take(8).collect::<String>() | |
| 1052 | + Uuid::new_v4() | |
| 1053 | + .to_string() | |
| 1054 | + .chars() | |
| 1055 | + .take(8) | |
| 1056 | + .collect::<String>() | |
| 1027 | 1057 | ); |
| 1028 | 1058 | psql( |
| 1029 | 1059 | "beta", |
@@ -1089,7 +1119,10 @@ async fn s3_untrusted_domain_is_rejected_with_403() {
| 1089 | 1119 | "beta", |
| 1090 | 1120 | "SELECT COUNT(*) FROM fediverse_trusted_domains WHERE domain='alpha.local'", |
| 1091 | 1121 | ); |
| 1092 | - assert_eq!(trust_count, "0", "alpha must be removed from beta's trust list"); | |
| 1122 | + assert_eq!( | |
| 1123 | + trust_count, "0", | |
| 1124 | + "alpha must be removed from beta's trust list" | |
| 1125 | + ); | |
| 1093 | 1126 | |
| 1094 | 1127 | // 2. Sign and send an AuthRequest from joanie@alpha to beta. |
| 1095 | 1128 | let alpha = joanie_alpha_keys(&client).await; |
@@ -1219,7 +1252,11 @@ fn set_beta_open_with_blocklist() -> Option<String> {
| 1219 | 1252 | VALUES (gen_random_uuid(), 'fediverse_trust_mode', 'open_with_blocklist', NULL, NOW()) \ |
| 1220 | 1253 | ON CONFLICT (key) DO UPDATE SET value='open_with_blocklist', updated_at=NOW()", |
| 1221 | 1254 | ); |
| 1222 | - if previous.is_empty() { None } else { Some(previous) } | |
| 1255 | + if previous.is_empty() { | |
| 1256 | + None | |
| 1257 | + } else { | |
| 1258 | + Some(previous) | |
| 1259 | + } | |
| 1223 | 1260 | } |
| 1224 | 1261 | |
| 1225 | 1262 | fn restore_beta_trust_mode(previous: Option<String>) { |
@@ -1322,7 +1359,10 @@ async fn s4_kill_domain_marks_remote_identities_disabled() {
| 1322 | 1359 | "SELECT COUNT(*) FROM remote_identities \ |
| 1323 | 1360 | WHERE domain='alpha.local' AND disabled_at IS NULL", |
| 1324 | 1361 | ); |
| 1325 | - assert_eq!(active_after, "0", "no active row must remain for alpha.local"); | |
| 1362 | + assert_eq!( | |
| 1363 | + active_after, "0", | |
| 1364 | + "no active row must remain for alpha.local" | |
| 1365 | + ); | |
| 1326 | 1366 | |
| 1327 | 1367 | // Restore for follow-on tests. |
| 1328 | 1368 | psql( |
@@ -1730,7 +1770,11 @@ async fn s6_update_person_refreshes_cached_display_name() {
| 1730 | 1770 | .send() |
| 1731 | 1771 | .await |
| 1732 | 1772 | .expect("Update POST must complete"); |
| 1733 | - assert_eq!(resp.status(), StatusCode::ACCEPTED, "Update(Person) must 202"); | |
| 1773 | + assert_eq!( | |
| 1774 | + resp.status(), | |
| 1775 | + StatusCode::ACCEPTED, | |
| 1776 | + "Update(Person) must 202" | |
| 1777 | + ); | |
| 1734 | 1778 | |
| 1735 | 1779 | let display = psql( |
| 1736 | 1780 | "beta", |
@@ -1787,7 +1831,11 @@ async fn s6_delete_person_disables_remote_identity_and_kills_active_jwt() {
| 1787 | 1831 | .send() |
| 1788 | 1832 | .await |
| 1789 | 1833 | .expect("Delete POST must complete"); |
| 1790 | - assert_eq!(resp.status(), StatusCode::ACCEPTED, "Delete(Person) must 202"); | |
| 1834 | + assert_eq!( | |
| 1835 | + resp.status(), | |
| 1836 | + StatusCode::ACCEPTED, | |
| 1837 | + "Delete(Person) must 202" | |
| 1838 | + ); | |
| 1791 | 1839 | |
| 1792 | 1840 | // 3. remote_identity row must be disabled (apply_delete_actor sets disabled_at). |
| 1793 | 1841 | let disabled = psql( |
@@ -1942,7 +1990,9 @@ async fn s1_consume_on_beta_mints_jwt_when_challenge_approved() {
| 1942 | 1990 | ); |
| 1943 | 1991 | |
| 1944 | 1992 | let body: Value = serde_json::from_str(&body_text).expect("consume body must be JSON"); |
| 1945 | - let jwt = body["jwt"].as_str().expect("consume response must carry jwt"); | |
| 1993 | + let jwt = body["jwt"] | |
| 1994 | + .as_str() | |
| 1995 | + .expect("consume response must carry jwt"); | |
| 1946 | 1996 | let parts: Vec<&str> = jwt.split('.').collect(); |
| 1947 | 1997 | assert_eq!(parts.len(), 3, "JWT must have header.payload.signature"); |
| 1948 | 1998 |
M
crates/rustwarden-fediverse/tests/fediverse_interop.rs
+4
-16
@@ -14,7 +14,6 @@
| 14 | 14 | //! a 503 leaves the row in `Pending` with `next_retry_at` set per |
| 15 | 15 | //! RFC 0010. |
| 16 | 16 | |
| 17 | - | |
| 18 | 17 | use std::sync::Arc; |
| 19 | 18 | |
| 20 | 19 | use async_trait::async_trait; |
@@ -39,13 +38,7 @@ use rustwarden_fediverse::services::fediverse_outbound_service::{
| 39 | 38 | /// Build the canonical `Signature` header for a draft-cavage signed |
| 40 | 39 | /// POST. Mirrors `HttpSignedDelivery::deliver` but lives in the test |
| 41 | 40 | /// module so the round-trip can be exercised without DB plumbing. |
| 42 | -fn sign_post( | |
| 43 | - private_pem: &str, | |
| 44 | - key_id: &str, | |
| 45 | - host: &str, | |
| 46 | - path: &str, | |
| 47 | - date: &str, | |
| 48 | -) -> String { | |
| 41 | +fn sign_post(private_pem: &str, key_id: &str, host: &str, path: &str, date: &str) -> String { | |
| 49 | 42 | let signed_headers = vec![ |
| 50 | 43 | "(request-target)".to_string(), |
| 51 | 44 | "host".to_string(), |
@@ -59,10 +52,8 @@ fn sign_post(
| 59 | 52 | }) |
| 60 | 53 | .expect("build_signature_input"); |
| 61 | 54 | |
| 62 | - let signature_bytes = | |
| 63 | - rsa_sha256_sign(private_pem, input.as_bytes()).expect("sign"); | |
| 64 | - let signature_b64 = | |
| 65 | - base64::engine::general_purpose::STANDARD.encode(&signature_bytes); | |
| 55 | + let signature_bytes = rsa_sha256_sign(private_pem, input.as_bytes()).expect("sign"); | |
| 56 | + let signature_b64 = base64::engine::general_purpose::STANDARD.encode(&signature_bytes); | |
| 66 | 57 | |
| 67 | 58 | format!( |
| 68 | 59 | r#"keyId="{key_id}",algorithm="rsa-sha256",headers="(request-target) host date",signature="{signature_b64}""#, |
@@ -151,10 +142,7 @@ struct InMemorySignedDelivery {
| 151 | 142 | |
| 152 | 143 | #[async_trait] |
| 153 | 144 | impl OutboundDelivery for InMemorySignedDelivery { |
| 154 | - async fn deliver( | |
| 155 | - &self, | |
| 156 | - item: &fediverse_outbound_queue::Model, | |
| 157 | - ) -> Result<(), AppError> { | |
| 145 | + async fn deliver(&self, item: &fediverse_outbound_queue::Model) -> Result<(), AppError> { | |
| 158 | 146 | let url = reqwest::Url::parse(&item.target_inbox) |
| 159 | 147 | .map_err(|e| AppError::Internal(format!("bad URL: {e}")))?; |
| 160 | 148 | let host = url |
M
crates/rustwarden-ui/src/handlers/dashboard_handler.rs
+1
-1
@@ -3,7 +3,7 @@
| 3 | 3 | //! Handles the dashboard widgets API endpoint. |
| 4 | 4 | //! Simplified for Rustwarden - no modular app system. |
| 5 | 5 | |
| 6 | -use axum::{response::IntoResponse, Json}; | |
| 6 | +use axum::{Json, response::IntoResponse}; | |
| 7 | 7 | use serde::Serialize; |
| 8 | 8 | |
| 9 | 9 | /// DTO for a dashboard widget (API response) |
M
crates/rustwarden-ui/src/handlers/email_handler.rs
+1
-1
@@ -10,10 +10,10 @@ use crate::dto::{
| 10 | 10 | EmailQueueEntry, EmailQueueListResponse, RetryEmailResponse, |
| 11 | 11 | }; |
| 12 | 12 | use axum::{ |
| 13 | + Json, | |
| 13 | 14 | extract::{Path, Query, State}, |
| 14 | 15 | http::StatusCode, |
| 15 | 16 | response::IntoResponse, |
| 16 | - Json, | |
| 17 | 17 | }; |
| 18 | 18 | use chrono::{DateTime, Utc}; |
| 19 | 19 | use rustwarden_core::error::AppError; |
M
crates/rustwarden-ui/src/handlers/health.rs
+1
-1
@@ -1,5 +1,5 @@
| 1 | 1 | use axum::{http::StatusCode, response::Json}; |
| 2 | -use serde_json::{json, Value}; | |
| 2 | +use serde_json::{Value, json}; | |
| 3 | 3 | |
| 4 | 4 | /// Health check endpoint handler |
| 5 | 5 | /// |
M
crates/rustwarden-ui/src/handlers/navigation_handler.rs
+1
-1
@@ -3,7 +3,7 @@
| 3 | 3 | //! Handles the navigation items API endpoint. |
| 4 | 4 | //! Simplified for Rustwarden - no modular app system. |
| 5 | 5 | |
| 6 | -use axum::{response::IntoResponse, Json}; | |
| 6 | +use axum::{Json, response::IntoResponse}; | |
| 7 | 7 | use serde::Serialize; |
| 8 | 8 | |
| 9 | 9 | /// DTO for a navigation item (API response) |
M
crates/rustwarden-ui/src/handlers/settings/preferences.rs
+2
-2
@@ -1,13 +1,13 @@
| 1 | 1 | //! Settings - User preferences (theme, language). |
| 2 | 2 | |
| 3 | 3 | use askama::Template; |
| 4 | -use axum::{extract::State, response::IntoResponse, Extension, Json}; | |
| 4 | +use axum::{Extension, Json, extract::State, response::IntoResponse}; | |
| 5 | 5 | use sea_orm::DatabaseConnection; |
| 6 | 6 | use serde::Deserialize; |
| 7 | 7 | use std::sync::Arc; |
| 8 | 8 | use tower_sessions::Session; |
| 9 | 9 | |
| 10 | -use super::{build_nav_items_from_dto, NavItem}; | |
| 10 | +use super::{NavItem, build_nav_items_from_dto}; | |
| 11 | 11 | use crate::dto::navigation_dto::NavigationDTO; |
| 12 | 12 | use crate::dto::user_profile_dto::UserProfileDTO; |
| 13 | 13 | use rustwarden_core::auth::jwt::Claims; |
M
crates/rustwarden-ui/src/handlers/settings/profile.rs
+2
-2
@@ -1,14 +1,14 @@
| 1 | 1 | //! Settings - Profile handlers (GET /settings/profile, PUT /settings/profile). |
| 2 | 2 | |
| 3 | 3 | use askama::Template; |
| 4 | -use axum::{extract::State, response::IntoResponse, Extension, Json}; | |
| 4 | +use axum::{Extension, Json, extract::State, response::IntoResponse}; | |
| 5 | 5 | use chrono::Utc; |
| 6 | 6 | use sea_orm::DatabaseConnection; |
| 7 | 7 | use serde::Deserialize; |
| 8 | 8 | use std::sync::Arc; |
| 9 | 9 | use tower_sessions::Session; |
| 10 | 10 | |
| 11 | -use super::{build_nav_items_from_dto, NavItem}; | |
| 11 | +use super::{NavItem, build_nav_items_from_dto}; | |
| 12 | 12 | use crate::dto::navigation_dto::NavigationDTO; |
| 13 | 13 | use crate::dto::user_profile_dto::UserProfileDTO; |
| 14 | 14 | use rustwarden_core::auth::jwt::Claims; |
M
crates/rustwarden-ui/src/handlers/settings/security.rs
+2
-2
@@ -2,16 +2,16 @@
| 2 | 2 | |
| 3 | 3 | use askama::Template; |
| 4 | 4 | use axum::{ |
| 5 | + Extension, Json, | |
| 5 | 6 | extract::{Query, State}, |
| 6 | 7 | response::IntoResponse, |
| 7 | - Extension, Json, | |
| 8 | 8 | }; |
| 9 | 9 | use sea_orm::DatabaseConnection; |
| 10 | 10 | use serde::Deserialize; |
| 11 | 11 | use std::sync::Arc; |
| 12 | 12 | use tower_sessions::Session; |
| 13 | 13 | |
| 14 | -use super::{build_nav_items_from_dto, filters, NavItem}; | |
| 14 | +use super::{NavItem, build_nav_items_from_dto, filters}; | |
| 15 | 15 | use crate::dto::navigation_dto::NavigationDTO; |
| 16 | 16 | use crate::dto::user_profile_dto::UserProfileDTO; |
| 17 | 17 | use rustwarden_core::auth::jwt::Claims; |
M
crates/rustwarden-ui/src/handlers/totp_handler.rs
+1
-1
@@ -5,9 +5,9 @@
| 5 | 5 | |
| 6 | 6 | use askama::Template; |
| 7 | 7 | use axum::{ |
| 8 | + Json, | |
| 8 | 9 | extract::{Extension, State}, |
| 9 | 10 | response::IntoResponse, |
| 10 | - Json, | |
| 11 | 11 | }; |
| 12 | 12 | use sea_orm::DatabaseConnection; |
| 13 | 13 | use serde::Deserialize; |
M
crates/rustwarden-ui/src/handlers/user_handler.rs
+1
-1
@@ -1,7 +1,7 @@
| 1 | 1 | use axum::{ |
| 2 | + Json, | |
| 2 | 3 | extract::{Query, State}, |
| 3 | 4 | http::StatusCode, |
| 4 | - Json, | |
| 5 | 5 | }; |
| 6 | 6 | use sea_orm::DatabaseConnection; |
| 7 | 7 | use serde::Deserialize; |
M
crates/rustwarden-ui/src/routes.rs
+8
-18
@@ -1,6 +1,6 @@
| 1 | 1 | use axum::{ |
| 2 | - routing::{delete, get, post, put}, | |
| 3 | 2 | Router, |
| 3 | + routing::{delete, get, post, put}, | |
| 4 | 4 | }; |
| 5 | 5 | use sea_orm::DatabaseConnection; |
| 6 | 6 | use tower_http::services::ServeDir; |
@@ -63,23 +63,14 @@ pub fn create_router() -> Router<DatabaseConnection> {
| 63 | 63 | .route("/home", get(home_handler::home)) |
| 64 | 64 | .route("/profile", get(profile::get_profile_page)) |
| 65 | 65 | .route("/profile", put(profile::update_profile)) |
| 66 | - .route( | |
| 67 | - "/settings/password", | |
| 68 | - get(security::get_password_page), | |
| 69 | - ) | |
| 66 | + .route("/settings/password", get(security::get_password_page)) | |
| 70 | 67 | .route("/settings/password", put(security::change_password)) |
| 71 | 68 | .route( |
| 72 | 69 | "/settings/password/pending", |
| 73 | 70 | delete(security::cancel_pending_password_change), |
| 74 | 71 | ) |
| 75 | - .route( | |
| 76 | - "/settings/sessions", | |
| 77 | - get(security::get_sessions_page), | |
| 78 | - ) | |
| 79 | - .route( | |
| 80 | - "/settings/sessions/{id}", | |
| 81 | - delete(security::revoke_session), | |
| 82 | - ) | |
| 72 | + .route("/settings/sessions", get(security::get_sessions_page)) | |
| 73 | + .route("/settings/sessions/{id}", delete(security::revoke_session)) | |
| 83 | 74 | .route( |
| 84 | 75 | "/settings/preferences", |
| 85 | 76 | get(preferences::get_preferences_page), |
@@ -132,11 +123,10 @@ pub fn create_router() -> Router<DatabaseConnection> {
| 132 | 123 | .route("/admin/settings", put(admin_handler::update_app_settings)) |
| 133 | 124 | .route("/admin/errors", get(admin_handler::admin_errors)); |
| 134 | 125 | |
| 135 | - let admin_routes = admin_routes | |
| 136 | - .route( | |
| 137 | - "/admin/users/{id}/reset-totp", | |
| 138 | - post(admin_handler::reset_user_totp), | |
| 139 | - ); | |
| 126 | + let admin_routes = admin_routes.route( | |
| 127 | + "/admin/users/{id}/reset-totp", | |
| 128 | + post(admin_handler::reset_user_totp), | |
| 129 | + ); | |
| 140 | 130 | |
| 141 | 131 | let static_dir = |
| 142 | 132 | std::env::var("RUSTWARDEN_STATIC_DIR").unwrap_or_else(|_| "static".to_string()); |
M
crates/rustwarden-ui/src/template_helpers.rs
+1
-1
@@ -7,8 +7,8 @@ use std::sync::Arc;
| 7 | 7 | use tower_sessions::Session; |
| 8 | 8 | |
| 9 | 9 | use crate::dto::{ |
| 10 | - navigation_dto::{NavigationDTO, NavigationItemDTO}, | |
| 11 | 10 | BreadcrumbItem, SidebarItem, SidebarSection, |
| 11 | + navigation_dto::{NavigationDTO, NavigationItemDTO}, | |
| 12 | 12 | }; |
| 13 | 13 | use rustwarden_core::auth::jwt::Claims; |
| 14 | 14 | use rustwarden_core::error::AppError; |
M
tools/rustwarden-cli/src/start_project.rs
+1
-2
@@ -641,8 +641,7 @@ fn copy_docs(
| 641 | 641 | project_dir: &Path, |
| 642 | 642 | fediverse_active: bool, |
| 643 | 643 | ) -> Result<(), Box<dyn std::error::Error>> { |
| 644 | - let mut docs_to_copy: Vec<&str> = | |
| 645 | - vec!["docs/SERVICES_REFERENCE.md", "docs/MIGRATION_GUIDE.md"]; | |
| 644 | + let mut docs_to_copy: Vec<&str> = vec!["docs/SERVICES_REFERENCE.md", "docs/MIGRATION_GUIDE.md"]; | |
| 646 | 645 | if fediverse_active { |
| 647 | 646 | docs_to_copy.push("docs/fediverse-consumer-guide.md"); |
| 648 | 647 | } |
M
tools/rustwarden-cli/src/templates.rs
+25
-29
@@ -134,17 +134,14 @@ pub fn cargo_toml(cfg: &ProjectConfig) -> String {
| 134 | 134 | // `--features fediverse` (or `fediverse-ui`, which implies it). |
| 135 | 135 | let features_section = match cfg.fediverse_mode { |
| 136 | 136 | FediverseMode::None => String::new(), |
| 137 | - FediverseMode::Headless => { | |
| 138 | - "\n[features]\n\ | |
| 137 | + FediverseMode::Headless => "\n[features]\n\ | |
| 139 | 138 | default = []\n\ |
| 140 | 139 | # Active le protocole ActivityPub headless (RFC 0001+).\n\ |
| 141 | 140 | # Compile et lie la crate `rustwarden-fediverse` (services, routes,\n\ |
| 142 | 141 | # signatures HTTP) sans aucune UI.\n\ |
| 143 | 142 | fediverse = [\"dep:rustwarden-fediverse\"]\n" |
| 144 | - .to_string() | |
| 145 | - } | |
| 146 | - FediverseMode::WithReferenceUi => { | |
| 147 | - "\n[features]\n\ | |
| 143 | + .to_string(), | |
| 144 | + FediverseMode::WithReferenceUi => "\n[features]\n\ | |
| 148 | 145 | default = []\n\ |
| 149 | 146 | # Active le protocole ActivityPub headless (RFC 0001+).\n\ |
| 150 | 147 | # Compile et lie la crate `rustwarden-fediverse` (services, routes,\n\ |
@@ -153,8 +150,7 @@ pub fn cargo_toml(cfg: &ProjectConfig) -> String {
| 153 | 150 | # Active l'UI HTML de reference pour la federation (handlers, templates,\n\ |
| 154 | 151 | # carte dashboard). Implique automatiquement `fediverse`.\n\ |
| 155 | 152 | fediverse-ui = [\"fediverse\", \"dep:rustwarden-fediverse-ui\"]\n" |
| 156 | - .to_string() | |
| 157 | - } | |
| 153 | + .to_string(), | |
| 158 | 154 | }; |
| 159 | 155 | |
| 160 | 156 | format!( |
@@ -238,10 +234,10 @@ pub fn main_rs(cfg: &ProjectConfig) -> String {
| 238 | 234 | // requise par le router fediverse, puis on injecte un second bloc |
| 239 | 235 | // `cfg(feature = "fediverse-ui")` quand l'UI de reference est |
| 240 | 236 | // demandee. |
| 241 | - let (builder_open, fediverse_block, fediverse_ui_block, builder_close) = | |
| 242 | - if cfg.has_fediverse() { | |
| 243 | - let fediverse_ui = if cfg.has_fediverse_ui() { | |
| 244 | - r#" | |
| 237 | + let (builder_open, fediverse_block, fediverse_ui_block, builder_close) = if cfg.has_fediverse() | |
| 238 | + { | |
| 239 | + let fediverse_ui = if cfg.has_fediverse_ui() { | |
| 240 | + r#" | |
| 245 | 241 | // Reference HTML UI for the federation. Opt-in via the |
| 246 | 242 | // `fediverse-ui` feature, which itself implies `fediverse`. Mounts |
| 247 | 243 | // the UI router and injects the federation card into the admin |
@@ -251,13 +247,13 @@ pub fn main_rs(cfg: &ProjectConfig) -> String {
| 251 | 247 | .merge_routes(rustwarden_fediverse_ui::default_routes()) |
| 252 | 248 | .add_dashboard_card(rustwarden_fediverse_ui::dashboard_card()); |
| 253 | 249 | "# |
| 254 | - } else { | |
| 255 | - "" | |
| 256 | - }; | |
| 250 | + } else { | |
| 251 | + "" | |
| 252 | + }; | |
| 257 | 253 | |
| 258 | - ( | |
| 259 | - "let builder = RustwardenApp::builder()\n", | |
| 260 | - r#"; | |
| 254 | + ( | |
| 255 | + "let builder = RustwardenApp::builder()\n", | |
| 256 | + r#"; | |
| 261 | 257 | |
| 262 | 258 | #[cfg(feature = "fediverse")] |
| 263 | 259 | let builder = { |
@@ -284,17 +280,17 @@ pub fn main_rs(cfg: &ProjectConfig) -> String {
| 284 | 280 | .merge_routes(fediverse_router) |
| 285 | 281 | }; |
| 286 | 282 | "#, |
| 287 | - fediverse_ui, | |
| 288 | - "\n builder\n .merge_routes(app_routes())\n .build()\n .await?\n .run()\n .await\n", | |
| 289 | - ) | |
| 290 | - } else { | |
| 291 | - ( | |
| 292 | - " RustwardenApp::builder()\n", | |
| 293 | - "", | |
| 294 | - "", | |
| 295 | - " .merge_routes(app_routes())\n .build()\n .await?\n .run()\n .await\n", | |
| 296 | - ) | |
| 297 | - }; | |
| 283 | + fediverse_ui, | |
| 284 | + "\n builder\n .merge_routes(app_routes())\n .build()\n .await?\n .run()\n .await\n", | |
| 285 | + ) | |
| 286 | + } else { | |
| 287 | + ( | |
| 288 | + " RustwardenApp::builder()\n", | |
| 289 | + "", | |
| 290 | + "", | |
| 291 | + " .merge_routes(app_routes())\n .build()\n .await?\n .run()\n .await\n", | |
| 292 | + ) | |
| 293 | + }; | |
| 298 | 294 | |
| 299 | 295 | let fediverse_use = if cfg.has_fediverse() { |
| 300 | 296 | "#[allow(unused_imports)]\nuse rustwarden_core::middleware::revocation::RemoteIdentityRevocationChecker;\n" |
M
tools/rustwarden-cli/src/update_project.rs
+12
-18
@@ -52,7 +52,9 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| 52 | 52 | let has_ui = project_dir.join("crates/rustwarden-ui").exists(); |
| 53 | 53 | let has_fediverse = project_dir.join("crates/rustwarden-fediverse").exists(); |
| 54 | 54 | let has_fediverse_ui = project_dir.join("crates/rustwarden-fediverse-ui").exists(); |
| 55 | - let has_fediverse_guard = project_dir.join("crates/rustwarden-fediverse-guard").exists(); | |
| 55 | + let has_fediverse_guard = project_dir | |
| 56 | + .join("crates/rustwarden-fediverse-guard") | |
| 57 | + .exists(); | |
| 56 | 58 | let custom_templates = detect_custom_templates(&project_dir, &rustwarden_root, has_ui); |
| 57 | 59 | |
| 58 | 60 | // Si la fédération (ou une de ses crates) manque, proposer de l'ajouter. |
@@ -533,19 +535,14 @@ fn print_summary(
| 533 | 535 | if newly_added.contains(&"rustwarden-fediverse-ui") { |
| 534 | 536 | println!( |
| 535 | 537 | " {}", |
| 536 | - style( | |
| 537 | - "fediverse-ui = [\"fediverse\", \"dep:rustwarden-fediverse-ui\"]" | |
| 538 | - ) | |
| 539 | - .green() | |
| 538 | + style("fediverse-ui = [\"fediverse\", \"dep:rustwarden-fediverse-ui\"]").green() | |
| 540 | 539 | ); |
| 541 | 540 | } |
| 542 | 541 | if newly_added.contains(&"rustwarden-fediverse-guard") { |
| 543 | 542 | println!( |
| 544 | 543 | " {}", |
| 545 | - style( | |
| 546 | - "fediverse-guard = [\"fediverse\", \"dep:rustwarden-fediverse-guard\"]" | |
| 547 | - ) | |
| 548 | - .green() | |
| 544 | + style("fediverse-guard = [\"fediverse\", \"dep:rustwarden-fediverse-guard\"]") | |
| 545 | + .green() | |
| 549 | 546 | ); |
| 550 | 547 | } |
| 551 | 548 | println!(); |
@@ -553,12 +550,8 @@ fn print_summary(
| 553 | 550 | " {} Reproduire le cablage de reference dans src/main.rs", |
| 554 | 551 | style("b)").cyan() |
| 555 | 552 | ); |
| 556 | - println!( | |
| 557 | - " (FediverseConfig::from_env, LocalUserResolver," | |
| 558 | - ); | |
| 559 | - println!( | |
| 560 | - " SignatureVerifier, register_revocation_checker," | |
| 561 | - ); | |
| 553 | + println!(" (FediverseConfig::from_env, LocalUserResolver,"); | |
| 554 | + println!(" SignatureVerifier, register_revocation_checker,"); | |
| 562 | 555 | println!(" merge_routes, OutboundWorker dans tokio::spawn)."); |
| 563 | 556 | println!(); |
| 564 | 557 | println!( |
@@ -579,7 +572,9 @@ fn print_summary(
| 579 | 572 | println!( |
| 580 | 573 | " {} Guide complet : {}", |
| 581 | 574 | style("→").dim(), |
| 582 | - style("docs/fediverse-consumer-guide.md").cyan().underlined() | |
| 575 | + style("docs/fediverse-consumer-guide.md") | |
| 576 | + .cyan() | |
| 577 | + .underlined() | |
| 583 | 578 | ); |
| 584 | 579 | println!( |
| 585 | 580 | " {} Reference API : {}", |
@@ -704,8 +699,7 @@ fn refresh_docs(
| 704 | 699 | project_dir: &Path, |
| 705 | 700 | has_fediverse: bool, |
| 706 | 701 | ) -> Result<(), Box<dyn std::error::Error>> { |
| 707 | - let mut docs_to_copy: Vec<&str> = | |
| 708 | - vec!["docs/SERVICES_REFERENCE.md", "docs/MIGRATION_GUIDE.md"]; | |
| 702 | + let mut docs_to_copy: Vec<&str> = vec!["docs/SERVICES_REFERENCE.md", "docs/MIGRATION_GUIDE.md"]; | |
| 709 | 703 | if has_fediverse { |
| 710 | 704 | docs_to_copy.push("docs/fediverse-consumer-guide.md"); |
| 711 | 705 | } |
GitRust