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(&current) {
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 }