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}