v0.5.0: Wake-on-LAN, webhook notifications, Advanced tab, login logo, update check
Closes the v0.4.x chapter — NFS works end to end. Five additions: ## Wake-on-LAN (Hosts → Bound hosts) - New core::wol module: parse any MAC form, build the 102-byte magic packet, broadcast it. No special capability needed (ephemeral source port; SO_BROADCAST). Sends to the limited broadcast (255.255.255.255) AND the server's own subnet broadcast (computed from advertised IP + detected mask) so it reaches the right VLAN. - POST /api/hosts/:mac/wol — only fires for *bound* MACs (404 otherwise) so it's not an open packet sprayer. - Bound-hosts table grows a "Wake" button with inline Waking…/Sent ✓ state. ## Webhook notifications (Advanced tab) - core::notify: NotifyConfig + NotifyStore (notify.json), one provider at a time — Slack / Discord / Teams (incoming-webhook JSON) or SMTP. SMTP password is persisted but redacted on GET behind a __keep__ sentinel the UI round-trips so the secret never leaves the box. - http-api::notify: delivery — reqwest POST for chat (provider-shaped bodies), lettre for SMTP (rustls, STARTTLS/implicit TLS, no plaintext). 10s timeout; every send is best-effort. - GET/PUT /api/notify, POST /api/notify/test. - Fired fire-and-forget on the canonical "machine is imaging" boot event and on WoL — never blocks the boot path. ## UI: Advanced tab - New nav item. Holds the webhook config card and the API reference block (relocated from the bottom of Settings). ## UI: login/setup logo (FleetDM treatment) - /api/me now returns has_custom_logo + logo_rev (public bootstrap). The login, setup, and connection-error cards render the uploaded logo full-width with the "OpenPXE" wordmark dropped — matching the sidebar. ## About: update check + licenses - "Check for updates" button → GET /api/updates/check queries the Gitea releases API (derived from CARGO_PKG_REPOSITORY) and compares to the running version. Strictly on-demand — no background polling, keeps the air-gapped promise. - License card documents the MIT OR Apache-2.0 dual license with links, plus a note on bundled components (iPXE GPLv2/UBDL, samba, wimtools). Deps: lettre (SMTP, rustls) + reqwest gains the json feature. Both rustls so the static musl binary stays OpenSSL-free. Tests: 179 passing (+notify round-trip/redaction, webhook validation, WoL-unbound-404, WoL packet loopback, version-compare). clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1eb41288c3
commit
fc99973ac3
@@ -102,6 +102,7 @@ async fn build_state() -> (AppState, tempfile::TempDir) {
|
||||
let branding = openpxe_core::BrandingStore::load_or_default(dir.path());
|
||||
let admin = openpxe_core::AdminStore::load_or_default(dir.path());
|
||||
let sso = openpxe_core::SsoStore::load_or_default(dir.path());
|
||||
let notify = openpxe_core::NotifyStore::load_or_default(dir.path());
|
||||
let sessions = openpxe_http_api::auth::SessionStore::default();
|
||||
let metrics = Metrics::new();
|
||||
let state = AppState {
|
||||
@@ -115,6 +116,7 @@ async fn build_state() -> (AppState, tempfile::TempDir) {
|
||||
admin,
|
||||
sessions,
|
||||
sso,
|
||||
notify,
|
||||
metrics,
|
||||
smb: None,
|
||||
smb_shares,
|
||||
@@ -535,6 +537,54 @@ async fn smb_shares_list_starts_empty() {
|
||||
|
||||
// v0.4.67: NFSv3 share manager (parallel to SMB).
|
||||
|
||||
// ── v0.5.0: notifications + Wake-on-LAN ────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_config_round_trips_and_redacts_smtp_password() {
|
||||
let (state, _dir) = build_state().await;
|
||||
let app = build_router(state);
|
||||
// Save an SMTP config with a password.
|
||||
let (s, _b) = put_json(
|
||||
&app,
|
||||
"/api/notify",
|
||||
r#"{"enabled":true,"kind":"smtp","smtp_host":"smtp.example.com","smtp_port":587,"smtp_to":"[email protected]","smtp_from":"[email protected]","smtp_password":"s3cret"}"#,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
// GET must redact the password (never echo the real secret).
|
||||
let (s, b) = get(&app, "/api/notify").await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
let v: serde_json::Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(v["enabled"], true);
|
||||
assert_eq!(v["kind"], "smtp");
|
||||
let pw = v["smtp_password"].as_str().unwrap_or("");
|
||||
assert_ne!(pw, "s3cret", "raw password must never be returned");
|
||||
assert!(!pw.is_empty(), "a set password should surface as a sentinel");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_enable_webhook_without_url_is_rejected() {
|
||||
let (state, _dir) = build_state().await;
|
||||
let app = build_router(state);
|
||||
let (s, _b) = put_json(
|
||||
&app,
|
||||
"/api/notify",
|
||||
r#"{"enabled":true,"kind":"slack","webhook_url":""}"#,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(s, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wol_on_unbound_mac_is_404() {
|
||||
// WoL only fires for bound MACs — an arbitrary MAC must 404 so the
|
||||
// endpoint isn't an open packet sprayer.
|
||||
let (state, _dir) = build_state().await;
|
||||
let app = build_router(state);
|
||||
let (s, _b) = post_json(&app, "/api/hosts/aa:bb:cc:dd:ee:ff/wol", "{}").await;
|
||||
assert_eq!(s, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn nfs_shares_list_starts_empty() {
|
||||
let (state, _dir) = build_state().await;
|
||||
|
||||
Reference in New Issue
Block a user