feat: add Keycloak authentication for dashboard and API endpoints (#2)
Some checks failed
CI / Format (push) Successful in 2s
CI / Security Audit (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Clippy (push) Has been cancelled

Dashboard: OAuth2/OIDC login flow with PKCE, session-based auth middleware
protecting all server function endpoints, check-auth server function for
frontend auth state, login page gate in AppShell, user info in sidebar.

Agent API: JWT validation middleware using Keycloak JWKS endpoint,
conditionally enabled when KEYCLOAK_URL and KEYCLOAK_REALM are set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com>
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-03-07 23:50:56 +00:00
parent 42cabf0582
commit 0cb06d3d6d
21 changed files with 741 additions and 13 deletions

View File

@@ -3,17 +3,57 @@ use dioxus::prelude::*;
use crate::app::Route;
use crate::components::sidebar::Sidebar;
use crate::components::toast::{ToastContainer, Toasts};
use crate::infrastructure::auth_check::check_auth;
#[component]
pub fn AppShell() -> Element {
use_context_provider(Toasts::new);
rsx! {
div { class: "app-shell",
Sidebar {}
main { class: "main-content",
Outlet::<Route> {}
let auth = use_server_future(check_auth)?;
match auth() {
Some(Ok(info)) if info.authenticated => {
use_context_provider(|| Signal::new(info.clone()));
rsx! {
div { class: "app-shell",
Sidebar {}
main { class: "main-content",
Outlet::<Route> {}
}
ToastContainer {}
}
}
}
Some(Ok(_)) => {
rsx! { LoginPage {} }
}
Some(Err(e)) => {
tracing::error!("Auth check failed: {e}");
rsx! { LoginPage {} }
}
None => {
rsx! {
div { class: "flex items-center justify-center h-screen bg-gray-950",
p { class: "text-gray-400", "Loading..." }
}
}
}
}
}
#[component]
fn LoginPage() -> Element {
rsx! {
div { class: "flex items-center justify-center h-screen bg-gray-950",
div { class: "text-center",
h1 { class: "text-3xl font-bold text-white mb-4", "Compliance Scanner" }
p { class: "text-gray-400 mb-8", "Sign in to access the dashboard" }
a {
href: "/auth",
class: "px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-500 transition-colors font-medium",
"Sign in with Keycloak"
}
}
ToastContainer {}
}
}
}

View File

@@ -1,3 +1,4 @@
use compliance_core::models::auth::AuthInfo;
use dioxus::prelude::*;
use dioxus_free_icons::icons::bs_icons::*;
use dioxus_free_icons::Icon;
@@ -114,8 +115,32 @@ pub fn Sidebar() -> Element {
Icon { icon: BsChevronLeft, width: 14, height: 14 }
}
}
if !collapsed() {
div { class: "sidebar-footer", "v0.1.0" }
{
let auth_info = use_context::<Signal<AuthInfo>>();
let info = auth_info();
let initials = info.name.chars().next().unwrap_or('U').to_uppercase().to_string();
rsx! {
div { class: "sidebar-user",
div { class: "user-avatar",
if info.avatar_url.is_empty() {
span { class: "avatar-initials", "{initials}" }
} else {
img { src: "{info.avatar_url}", alt: "avatar", class: "avatar-img" }
}
}
if !collapsed() {
div { class: "user-info",
span { class: "user-name", "{info.name}" }
a {
href: "/logout",
class: "logout-link",
Icon { icon: BsBoxArrowRight, width: 14, height: 14 }
" Logout"
}
}
}
}
}
}
}
}