frontend/
auth.rs

1use gloo_net::http::Request;
2use wasm_bindgen_futures::spawn_local;
3use yew::prelude::*;
4use yew_router::prelude::*;
5
6/// Represents the authentication state of the current user.
7///
8/// This struct holds information about whether a user is authenticated and if they
9/// possess administrator privileges.
10///
11/// # Fields
12/// - `is_authenticated`: An `Option<bool>` indicating if the user is logged in.
13///   `None` means the status is still being checked.
14/// - `is_admin`: An `Option<bool>` indicating if the authenticated user is an administrator.
15///   `None` means the admin status is still being checked or is not applicable.
16#[derive(Clone, Debug, PartialEq)]
17pub struct AuthState {
18    pub is_authenticated: Option<bool>,
19    pub is_admin: Option<bool>,
20}
21
22/// Properties for the [`ProtectedRoute`] component.
23///
24/// # Fields
25/// - `children`: The child components that this protected route will render if access is granted.
26/// - `admin_page`: A boolean flag indicating whether this route requires administrator privileges.
27///   If `true`, the user must be authenticated AND be an administrator to access the `children`.
28#[derive(Properties, PartialEq)]
29pub struct ProtectedRouteProps {
30    pub children: Children,
31    pub admin_page: bool,
32}
33
34/// A component that protects routes by enforcing authentication and optional administrator privileges.
35///
36/// This component uses the backend's validation middleware by fetching the current user's authentication
37/// and admin status from the `/api/users/current` endpoint (which requires a valid JWT token).
38/// Based on the [`AuthState`] and the `admin_page` property, it either renders its children or redirects the user.
39///
40/// # Behavior
41/// - **Initial Load**: Displays "Loading..." while checking authentication status via the backend.
42/// - **Not Authenticated**: Redirects to the login page (`crate::Route::Login`).
43/// - **Authenticated** (valid JWT token from backend):
44///   - If `admin_page` is `true`:
45///     - If the user is an administrator (`is_admin: Some(true)`), it renders `children`.
46///     - If the user is not an administrator (`is_admin: Some(false)`), it redirects to
47///       the permission denied page (`crate::Route::PermissionDenied`).
48///     - If admin status is still being checked (`is_admin: None`), it displays "Checking permissions...".
49///   - If `admin_page` is `false`: It renders `children` directly, as only authentication is required.
50///
51/// # Example Usage
52/// ```ignore
53/// html! {
54///     <ProtectedRoute admin_page={true}>
55///         <AdminDashboard />
56///     </ProtectedRoute>
57/// }
58/// ```
59#[component(ProtectedRoute)]
60pub fn protected_route(props: &ProtectedRouteProps) -> Html {
61    let auth_state = use_state(|| AuthState {
62        is_authenticated: None,
63        is_admin: None,
64    });
65
66    {
67        let auth_state = auth_state.clone();
68        use_effect_with((), move |_| {
69            let auth_state = auth_state.clone();
70            spawn_local(async move {
71                match Request::get("/api/users/current")
72                    .credentials(web_sys::RequestCredentials::Include)
73                    .send()
74                    .await
75                {
76                    Ok(resp) => {
77                        let status = resp.status();
78                        web_sys::console::log_1(&format!("Auth check: status {}", status).into());
79                        if status == 200 {
80                            let user_data: serde_json::Value =
81                                resp.json().await.unwrap_or_default();
82                            let is_admin = user_data["data"]["is_admin"].as_bool();
83
84                            auth_state.set(AuthState {
85                                is_authenticated: Some(true),
86                                is_admin,
87                            });
88                        } else {
89                            auth_state.set(AuthState {
90                                is_authenticated: Some(false),
91                                is_admin: Some(false),
92                            });
93                        }
94                    }
95                    Err(err) => {
96                        web_sys::console::log_1(&format!("Auth check error: {:?}", err).into());
97                        auth_state.set(AuthState {
98                            is_authenticated: Some(false),
99                            is_admin: Some(false),
100                        });
101                    }
102                }
103            });
104            || ()
105        });
106    }
107
108    match *auth_state {
109        AuthState {
110            is_authenticated: None,
111            ..
112        } => html! { <div>{ "Wird geladen..." } </div> },
113        AuthState {
114            is_authenticated: Some(false),
115            ..
116        } => html! {
117            <Redirect<crate::Route> to={crate::Route::Login}/>
118        },
119        AuthState {
120            is_authenticated: Some(true),
121            is_admin: admin_flag,
122        } => {
123            if props.admin_page {
124                match admin_flag {
125                    Some(true) => props.children.clone().into(),
126                    Some(false) => {
127                        html! { <Redirect<crate::Route> to={crate::Route::PermissionDenied}/> }
128                    }
129                    None => html! { <div>{ "Überprüfe Berechtigungen..." }</div> },
130                }
131            } else {
132                props.children.clone().into()
133            }
134        }
135    }
136}