4 Commits

Author SHA1 Message Date
restitux aa2d92a7ae frontend: add admin panel for user and permission management
Admin-only page at /admin with:
- Create user form (username, password, admin toggle)
- User list table with edit and delete actions
- Inline user editing (change password, toggle admin role)
- Inline permission editor with per-app checkboxes grouped by server
- Access guarded by checking is_admin from /api/auth/me

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:36:29 +00:00
restitux 786579a7d8 frontend: attach auth credentials to all API requests
Add Authorization Bearer header to all fetch calls (apps, stream
start). Handle 401 responses by clearing token and redirecting to
login. Pass stream_token from the stream start response through to
the WebTransport URL as a query parameter for proxy authentication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:36:29 +00:00
restitux af9359bbdf frontend: add login page and auth guard
Add authentication flow to the frontend:
- authStore with token management (localStorage persistence)
- Login page with username/password form at /login
- Layout-level auth guard that redirects to /login when no valid
  session exists, validates token on load via GET /api/auth/me
- Top navigation bar showing username and admin link when applicable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:36:29 +00:00
restitux b8c705554f backend: add single-use token auth for spawned stream proxies
Generate a random 256-bit token when spawning a proxy process, pass
it as a CLI argument, and return it to the client in the stream start
response. The proxy validates the token on WebTransport connect and
consumes it after first use, preventing replay. A wrong token attempt
also consumes the token for security. Includes 5 unit tests for token
validation logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:36:29 +00:00
@@ -85,31 +85,17 @@ impl crate::proxy::Proxy {
description: "Could not start stream".to_string(), description: "Could not start stream".to_string(),
}); });
// Validate single-use stream token // Validate single-use stream token via the shared helper so this
// handler and its unit tests exercise the same code path.
let provided_token = req.query::<String>("token").unwrap_or_default(); let provided_token = req.query::<String>("token").unwrap_or_default();
{ if let Err(msg) = super::validate_stream_token(&self, &provided_token).await {
let mut token_guard = self.stream_token.write().await; error!("Stream token validation failed: {msg}");
match token_guard.take() { return Err(AppError {
Some(expected) if expected == provided_token => { status_code: StatusCode::UNAUTHORIZED,
// Token consumed successfully (single-use) description: msg,
info!("Stream token validated and consumed"); });
}
Some(_) => {
error!("Invalid stream token provided");
return Err(AppError {
status_code: StatusCode::UNAUTHORIZED,
description: "Invalid stream token".to_string(),
});
}
None => {
error!("Stream token already consumed");
return Err(AppError {
status_code: StatusCode::UNAUTHORIZED,
description: "Stream token already used".to_string(),
});
}
}
} }
info!("Stream token validated and consumed");
info!("WebTransport connection initiated"); info!("WebTransport connection initiated");
let (wt_stream_send, wt_stream_recv, wt_datagram_send) = match setup_webtransport(req).await let (wt_stream_send, wt_stream_recv, wt_datagram_send) = match setup_webtransport(req).await