feat(backend): appareils appairés persistants, révocables et code éphémère (#77 B1-B4)
L'appairage ne survivait pas au redémarrage et son code, permanent, était imprimé sur la sortie standard. Un appareil appairé devient une entité persistante, nommée et révocable, derrière un code désormais éphémère. - B1 : port DeviceSessionStore et adapter FsDeviceSessionStore, entités de domaine (PairedDevice, DeviceId, SessionTokenHash, DeviceName). Les tokens sont hachés en SHA-256 et comparés en temps constant (subtle) : le store ne peut pas rejouer une session qu'il a servie. Cookie Max-Age 400 j à renouvellement glissant, lastSeenAtMs throttlé. - B2 : code éphémère en mémoire, TTL 10 min et usage unique, toute génération invalidant la précédente. POST /api/pairing-code authentifiée, flag --new-code. Le code est retiré du boot et l'eprintln! qui l'imprimait est supprimé. - B3 : endpoints devices (list/rename/revoke/revoke-all/logout), event DeviceRevoked et ActiveConnectionRegistry par device_id, fermant sans délai les WebSockets d'un appareil révoqué. - B4 : port PairAttemptLimiter et adapter mémoire, rate-limit par origine et global sur horloge injectée, donc testable sans attente réelle. La normalisation du code passe côté serveur : elle absorbe la dette #76, que la seule normalisation frontend de #75 ne faisait que masquer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -10,7 +10,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use backend::BackendCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use web_server::{EmbeddedServerHandle, ServerConfig, TrustedProxy};
|
||||
use web_server::{EmbeddedServerHandle, PairingCodeDto, ServerConfig, TrustedProxy};
|
||||
|
||||
use crate::dto::ErrorDto;
|
||||
|
||||
@ -93,8 +93,6 @@ pub struct EmbeddedServerStatusDto {
|
||||
pub public_url: Option<String>,
|
||||
/// Reverse-proxy upstream URL derived from settings.
|
||||
pub upstream_url: Option<String>,
|
||||
/// Runtime pairing code, never persisted.
|
||||
pub pairing_code: Option<String>,
|
||||
/// Last failure, when state is `failed`.
|
||||
pub error: Option<ErrorDto>,
|
||||
}
|
||||
@ -278,6 +276,21 @@ impl EmbeddedServerController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a new ephemeral pairing code on the running embedded server.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] if the embedded server is not running.
|
||||
pub fn generate_pairing_code(&self) -> Result<PairingCodeDto, ErrorDto> {
|
||||
let inner = self.inner.lock().expect("embedded server mutex poisoned");
|
||||
let Some(handle) = inner.handle.as_ref() else {
|
||||
return Err(ErrorDto {
|
||||
code: "UNAVAILABLE".to_owned(),
|
||||
message: "embedded server is not running".to_owned(),
|
||||
});
|
||||
};
|
||||
Ok(handle.generate_pairing_code())
|
||||
}
|
||||
|
||||
/// Stops the embedded server. Idempotent when already stopped.
|
||||
///
|
||||
/// # Errors
|
||||
@ -331,10 +344,6 @@ fn status_from_inner(inner: &EmbeddedServerInner) -> EmbeddedServerStatusDto {
|
||||
local_url: inner.handle.as_ref().map(|handle| handle.url().to_owned()),
|
||||
public_url: inner.public_url.clone(),
|
||||
upstream_url: inner.upstream_url.clone(),
|
||||
pairing_code: inner
|
||||
.handle
|
||||
.as_ref()
|
||||
.map(|handle| handle.pairing_code().to_owned()),
|
||||
error,
|
||||
}
|
||||
}
|
||||
@ -436,6 +445,7 @@ fn server_config_from_settings(
|
||||
trusted_proxies,
|
||||
app_data_dir,
|
||||
web_root,
|
||||
new_code: false,
|
||||
};
|
||||
config.validate().map_err(invalid_error)?;
|
||||
Ok(config)
|
||||
@ -771,7 +781,9 @@ mod tests {
|
||||
.as_deref()
|
||||
.is_some_and(|url| url.starts_with("http://127.0.0.1:")));
|
||||
assert_ne!(first.local_url.as_deref(), Some("http://127.0.0.1:0"));
|
||||
assert_eq!(first.pairing_code, second.pairing_code);
|
||||
let pairing = controller.generate_pairing_code().unwrap();
|
||||
assert_eq!(pairing.ttl_seconds, 600);
|
||||
assert_eq!(pairing.code.len(), 8);
|
||||
|
||||
let stopped = controller.stop().await.unwrap();
|
||||
|
||||
@ -780,7 +792,6 @@ mod tests {
|
||||
EmbeddedServerStatusStateDto::Stopped
|
||||
));
|
||||
assert!(stopped.local_url.is_none());
|
||||
assert!(stopped.pairing_code.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -804,6 +815,5 @@ mod tests {
|
||||
|
||||
assert_eq!(err.code, "INVALID");
|
||||
assert!(matches!(status.state, EmbeddedServerStatusStateDto::Failed));
|
||||
assert!(status.pairing_code.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user