use gloo_net::http::Request; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; /// Represents the authentication state of the current user. /// /// This struct holds information about whether a user is authenticated and if they /// possess administrator privileges. /// /// # Fields /// - `is_authenticated`: An `Option` indicating if the user is logged in. /// `None` means the status is still being checked. /// - `is_admin`: An `Option` indicating if the authenticated user is an administrator. /// `None` means the admin status is still being checked or is not applicable. #[derive(Clone, Debug, PartialEq)] pub struct AuthState { pub is_authenticated: Option, pub is_admin: Option, } /// Properties for the [`ProtectedRoute`] component. /// /// # Fields /// - `children`: The child components that this protected route will render if access is granted. /// - `admin_page`: A boolean flag indicating whether this route requires administrator privileges. /// If `true`, the user must be authenticated AND be an administrator to access the `children`. #[derive(Properties, PartialEq)] pub struct ProtectedRouteProps { pub children: Children, pub admin_page: bool, } /// A component that protects routes by enforcing authentication and optional administrator privileges. /// /// This component uses the backend's validation middleware by fetching the current user's authentication /// and admin status from the `/api/users/current` endpoint (which requires a valid JWT token). /// Based on the [`AuthState`] and the `admin_page` property, it either renders its children or redirects the user. /// /// # Behavior /// - **Initial Load**: Displays "Loading..." while checking authentication status via the backend. /// - **Not Authenticated**: Redirects to the login page (`crate::Route::Login`). /// - **Authenticated** (valid JWT token from backend): /// - If `admin_page` is `true`: /// - If the user is an administrator (`is_admin: Some(true)`), it renders `children`. /// - If the user is not an administrator (`is_admin: Some(false)`), it redirects to /// the permission denied page (`crate::Route::PermissionDenied`). /// - If admin status is still being checked (`is_admin: None`), it displays "Checking permissions...". /// - If `admin_page` is `false`: It renders `children` directly, as only authentication is required. /// /// # Example Usage /// ```ignore /// html! { /// /// /// /// } /// ``` #[component(ProtectedRoute)] pub fn protected_route(props: &ProtectedRouteProps) -> Html { let auth_state = use_state(|| AuthState { is_authenticated: None, is_admin: None, }); { let auth_state = auth_state.clone(); use_effect_with((), move |_| { let auth_state = auth_state.clone(); spawn_local(async move { match Request::get("/api/users/current") .credentials(web_sys::RequestCredentials::Include) .send() .await { Ok(resp) => { let status = resp.status(); web_sys::console::log_1(&format!("Auth check: status {}", status).into()); if status == 200 { let user_data: serde_json::Value = resp.json().await.unwrap_or_default(); let is_admin = user_data["data"]["is_admin"].as_bool(); auth_state.set(AuthState { is_authenticated: Some(true), is_admin, }); } else { auth_state.set(AuthState { is_authenticated: Some(false), is_admin: Some(false), }); } } Err(err) => { web_sys::console::log_1(&format!("Auth check error: {:?}", err).into()); auth_state.set(AuthState { is_authenticated: Some(false), is_admin: Some(false), }); } } }); || () }); } match *auth_state { AuthState { is_authenticated: None, .. } => html! {
{ "Loading..." }
}, AuthState { is_authenticated: Some(false), .. } => html! { to={crate::Route::Login}/> }, AuthState { is_authenticated: Some(true), is_admin: admin_flag, } => { if props.admin_page { match admin_flag { Some(true) => props.children.clone().into(), Some(false) => { html! { to={crate::Route::PermissionDenied}/> } } None => html! {
{ "Checking permissions..." }
}, } } else { props.children.clone().into() } } } }