Skip to content

feat(wifi): add WiFi management and fix portal auth on set-password#105

Open
JanZachmann wants to merge 16 commits intoomnect:mainfrom
JanZachmann:feat/wifi-management
Open

feat(wifi): add WiFi management and fix portal auth on set-password#105
JanZachmann wants to merge 16 commits intoomnect:mainfrom
JanZachmann:feat/wifi-management

Conversation

@JanZachmann
Copy link
Contributor

Summary

  • feat(wifi): Add WiFi management via wifi-commissioning-gatt service — scan for networks, connect, forget, and view current connection status from the Network page
  • fix(auth): Validate portal token in requiresPortalAuth router guard — prevents "portal authentication required" when submitting the set-password form after a factory reset (regular browser tab; incognito was unaffected)
  • fix(auth): Defer WiFi data fetch until after authentication
  • refactor(wifi): Remove redundant status and IP display from WiFi connection section
  • fix(wifi): Add missing state field to scan results response
  • fix(scripts): Disable SSH host key checking for device deployment script

Reason

WiFi management: Devices with a wifi-commissioning-gatt service need a UI surface to configure WiFi without physical access to the device.

Portal auth bug: After a factory reset, the OIDC user persists in localStorage. The requiresPortalAuth guard on /set-password saw a valid (non-expired) OIDC user and passed — but Callback.vue was bypassed, so portal_validated was never set on the fresh backend session. The set_password endpoint returned 401. Fix: the guard now calls token/validate to establish the backend session flag before allowing access. If validation fails (stale session), the stale OIDC user is removed and a fresh Keycloak flow is triggered.

Verification

  • All 90 E2E tests pass (./scripts/run-e2e-tests.sh)
  • Factory reset reconnection flow covered end-to-end: set-password redirect → password submission → dashboard → factory reset success modal
  • Auth stale-session test updated to reflect that the guard now catches this before the form is shown
  • WiFi tests in src/ui/tests/wifi.spec.ts

@JanZachmann JanZachmann force-pushed the feat/wifi-management branch 4 times, most recently from 531374d to 1f082af Compare March 2, 2026 21:43
@JanZachmann JanZachmann force-pushed the feat/wifi-management branch from 1f082af to 0fd7d6e Compare March 4, 2026 10:38
- **WiFi Management:** Implemented WiFi scanning, connection, and PSK calculation (WPA-PSK/WPA2-PSK) in the Crux Core and updated the UI to support wireless network configuration via wifi-commissioning-service.
- **Model & Synchronization:** Moved factoryResetResultAcked and updateValidationAcked from the persistent Healthcheck state to ephemeral sessionStorage. This ensures that these one-time notification flags do not persist across device reboots or accidental state synchronization cycles, preventing redundant modal popups.
- **Core Logic:** Implemented FetchInitialHealthcheck in the Crux Core to decouple the initial application hydration from subsequent polling cycles. This allows for a deterministic startup sequence where the UI can react to the device state immediately after WASM initialization.
- **Test Infrastructure:**
    - Refactored setupAndLogin and NetworkTestHarness to utilize sessionStorage for modal suppression, replacing the fragile healthcheck injection pattern.
    - Introduced setupWithLogin and mockNewIpProxy in the harness to reduce boilerplate and improve reliability of complex network redirection tests.
    - Standardized healthcheck mocking across all E2E tests to ensure consistent behavior during version-mismatch and rollback scenarios.
- **Backend & Utilities:** Added parse_json_response_any_status to handle cases where the backend returns valid JSON alongside non-2xx status codes (e.g., 503 during version mismatch).

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
- **Session Restore:** Implemented session persistence across page refreshes using a server-side cookie. During WASM initialization, the UI now attempts to refresh the session via `GET /token/refresh`.
- **Backend Persistence:** Introduced `SessionKeyService` to persist the session signing key in `/data/session.key`, ensuring sessions remain valid across backend restarts.
- **Route Guards & Navigation:**
    - Improved the `/set-password` guard to avoid redundant OIDC round-trips by checking authentication and password status.
    - Added a catch-all route that redirects unknown paths to the root, which is then handled by existing authentication guards.
- **API Reliability:** Ensured the token refresh endpoint returns the correct `text/plain` content type to satisfy frontend security checks.
- **Test Infrastructure:** Expanded E2E coverage for session restore, route guards, and cookie handling, including tighter validation of session cookies in mocks.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann JanZachmann force-pushed the feat/wifi-management branch from 0fd7d6e to f326f29 Compare March 4, 2026 15:37
Adds a Settings page that allows users to view and modify the timeout
values for device operations (reboot, factory reset, firmware update,
network rollback). Settings are persisted on the backend via a JSON
file and loaded on startup.

- Backend: new settings service with get/save endpoints (`/settings`)
- Core: new `TimeoutSettings` type, `SaveSettings` event and handler;
  model carries `timeout_settings` loaded from backend on init
- Update default firmware update timeout to 900s

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Drastically reduces the Docker image size (~71MB reduction, from 106MB to 34.9MB)
by removing the external Centrifugo binary and its dependencies.

- **Backend:** Integrated `actix-ws` into the Rust backend. Added a dual-port
  listener: HTTPS (1977) for user traffic and local HTTP (8000) for internal
  device status publishing from `omnect-device-service`.
- **Core:** Replaced magic strings with a type-safe `WebSocketChannel` enum.
  Refactored all Centrifugo references to generic WebSocket terminology.
- **Frontend:** Replaced the `centrifuge` npm package with a native WebSocket
  client (`useWebSocket.ts`).
- **Reliability:** Implemented a `republish` sync mechanism to ensure the UI
  state is perfectly updated upon connection, compensating for the lack of
  Centrifugo's message history.
- **Cleanup:** Removed obsolete scripts (`setup-centrifugo.sh`), configurations,
  and documentation references to Centrifugo.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
- Standardized all page and section headers to text-h4 format.
- Improved metric alignment on Device page using a nested grid layout.
- Compacted network adapter sidebar by reducing tab widths and padding.
- Added navigation icons to sidebar routes and Documentation link.
- Implemented reliable autofocus for password fields via template refs.
- Restored semantic <h1> tags on password pages to fix failing E2E tests.
- Fixed layout padding on Device Overview to prevent header truncation.
- Updated UI dependencies (Vuetify 4, UnoCSS 66.6.5) and workspace crates.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
- Split dual-bind server into isolated HTTPS (UI) and HTTP (ODS publish)
  servers to prevent exposing UI routes on unencrypted HTTP
- Add AuthMw to /ws, /network, /republish, /ack-rollback,
  /ack-factory-reset-result, /ack-update-validation, POST /api/settings
- Clear WebSocket subscriptions on disconnect to prevent memory leaks
- Remove dead history() method (no-op after Centrifugo removal)
- Delete accidentally committed .bak file
- Add backend integration tests for route auth enforcement
- Add E2E tests for route guards and subscription lifecycle

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann JanZachmann force-pushed the feat/wifi-management branch from 550bda0 to 1eb4829 Compare March 6, 2026 14:30
Implement WiFi commissioning integration: version-aware availability
check (requires >= 0.1.0), fix VERSION_ENDPOINT to /api/v1/version,
move WiFi DTOs to shared core types, add UI display of service version
with compatibility hint, and extend e2e tests for WiFi availability
states.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Prior to this, every unit test in src/app/src/update/ discarded the
returned Command, verifying only model state. A handler could send
requests to the wrong URL or with wrong headers without any test
catching it.

Using the new Command testing API introduced in crux_core 0.17.0-rc3
(expect_one_effect, expect_effect, expect_http, expect_web_socket,
split), add 7 new tests that verify the produced effects:

- auth: Login POSTs to /token/login with Basic auth header
- auth: Logout POSTs to /logout with Bearer auth header
- auth: CheckRequiresPasswordSet GETs /require-set-password
- websocket: SubscribeToChannels emits WebSocketOperation::SubscribeAll
- websocket: UnsubscribeFromChannels emits WebSocketOperation::UnsubscribeAll
- reconnection: ReconnectionCheckTick GETs /healthcheck
- wifi: CheckAvailability GETs /wifi/available

Single-effect commands use the generated expect_http()/expect_web_socket()
helpers directly (matching the upstream counter example pattern). Multi-
effect commands (Command::all([render, http])) collect both effects and
use find_map to locate the Http effect, since no single-variant helper
applies when the effect type is unknown upfront.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
…ust 2024 edition

Move all Shell-owned setInterval polling loops into Core-owned self-rescheduling
crux_time timers. The four pollers (WiFi scan 500ms, WiFi connect 1s, reconnection
5s, new IP 5s) are now scheduled by Core using TimeCmd::notify_after; each tick
handler re-schedules the next tick or returns Command::done() when polling should
stop. Page-refresh resilience is handled via #[serde(skip)] bool flags in Model
that gate the first poll on WebSocket delivery of an active operation state.

Shell changes: Remove four setInterval/clearInterval blocks and checkPendingNetworkChange
from timers.ts; add time.ts Time-capability handler with VITE_RECONNECTION_POLL_INTERVAL_MS
build-time cap so E2E test builds fire polls at 500ms instead of 5s; wire EffectVariantTime
in effects.ts.

Also adopts Rust 2024 edition for the Core crate (edition.workspace = true) which
enables let-chains used throughout the update handlers.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Adopts std::sync::LazyLock instead of lazy_static, simplifies Future
prelude usage, and documents future-facing edition improvements in
rust-2024.md.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Add countdown tick event variants (ReconnectionCountdownTick,
NewIpCountdownTick) and OverlaySpinnerState::decrement_countdown(),
completing the move of all polling and countdown scheduling from the
Vue Shell to Core via crux_time.

Remove the now-redundant timers.ts shell module; its sole remaining
responsibility (navigation on newIpReachable) is inlined into sync.ts
where it fits naturally alongside other post-sync side-effects.

Replace the per-operation timeout env vars in run-e2e-tests.sh with a
unified VITE_RECONNECTION_TIMEOUT_MS cap for Core-driven one-shot timers.

Add effect-level assertions to Core unit tests covering all TimeRequest
NotifyAfter schedules: poll intervals, countdown ticks, and configurable
operation timeouts across reconnection.rs, operations.rs, and
verification.rs.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
… change

When the device IP changes, the UI polls /healthcheck on the new IP from
the old IP's origin — a cross-origin request. Two issues blocked this:

1. `credentials: 'include'` was hardcoded globally in http.ts. Browsers
   reject `Access-Control-Allow-Origin: *` when credentials mode is
   'include', so the response was blocked despite returning HTTP 200.
   Changed to `credentials: 'same-origin'`: same-origin requests (all
   authenticated API calls) still send cookies; cross-origin ones do not,
   which is correct since the healthcheck is unauthenticated.

2. /healthcheck was missing the Access-Control-Allow-Origin: * header,
   so browsers blocked the cross-origin read entirely. Added the header
   to all response branches.

Without this fix, the poll always fails → no redirect to the new IP →
no login → POST /token/login never fires → cancel_rollback() is never
called → ODS rolls back the network config after the timeout.

Also improve two e2e tests:
- "rollback cancellation" test: use waitForURL instead of manual harness
  state assertions to verify the redirect actually occurs
- "DHCP -> Static (New IP)": exercise the full flow including redirect
- "DHCP -> Static (Same IP)": assert overlay clears after healthcheck
  succeeds at the same host

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
When the Network page mounts, no adapter was pre-selected, leaving the
content area empty until the user manually clicked a tab. This occurred
on every page load since networkStatus arrives async via WebSocket, and
was especially visible after an IP change caused a browser redirect and
re-login.

Adds a watch on networkStatus that selects the current connection adapter
(matched by browser hostname) or falls back to the first adapter in the
list, once, when the tab is still unset.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Update default socket paths for device service and wifi commissioning to
match the actual paths used on the device. Remove the centrifugo config
file that is no longer part of this repository.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Documents all connections, transport security, authentication flows,
session/token properties, authorization model, credential storage,
public vs. protected endpoints, and security considerations.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant