frontend/
lib.rs

1mod auth;
2mod darkmode;
3mod pages;
4use crate::auth::ProtectedRoute;
5use crate::pages::*;
6use gloo_net::http::Request;
7use wasm_bindgen_futures::spawn_local;
8use yew::prelude::*;
9use yew_router::prelude::*;
10
11/// Defines the application's various routes and their corresponding paths.
12///
13/// This enum is used by `yew-router` to map URLs to specific components,
14/// enabling navigation within the single-page application. Each route is protected
15/// by [`ProtectedRoute`] middleware where appropriate to enforce authentication and authorization.
16/// See [`switch`] for the routing logic.
17#[derive(Clone, PartialEq, Routable)]
18enum Route {
19    /// The application's home page.
20    #[at("/")]
21    Home,
22    /// Route for submitting a new ticket.
23    #[at("/ticket")]
24    Ticket,
25    /// Route for viewing a specific ticket by its ID.
26    #[at("/tickets/:id")]
27    TicketById { id: i32 },
28    /// Route for viewing all tickets.
29    #[at("/tickets")]
30    AllTickets,
31    /// Route for viewing archived tickets.
32    #[at("/tickets/archive")]
33    ArchivedTickets,
34    /// Route for user registration.
35    #[at("/register")]
36    Register,
37    /// Route for user login.
38    #[at("/login")]
39    Login,
40    /// Route for the initial administrator setup.
41    #[at("/setup")]
42    Setup,
43    /// Route for viewing all users.
44    #[at("/users")]
45    AllUsers,
46    /// Route for viewing a specific user by their ID.
47    #[at("/users/:id")]
48    UserByID { id: i16 },
49    /// Route for displaying diagnostics information (admin-only).
50    #[at("/diagnostics")]
51    Diagnostics,
52    /// Route displayed when a user attempts to access a page without sufficient permissions.
53    #[at("/denied")]
54    PermissionDenied,
55    /// Catch-all route for unmatched paths, leading to a 404 Not Found page.
56    #[not_found]
57    #[at("/404")]
58    NotFound,
59}
60
61/// Properties for the [`SidebarShell`] component.
62#[derive(Properties, PartialEq)]
63pub struct SidebarShellProps {
64    /// The child components to be rendered within the main content area of the shell.
65    pub children: Children,
66}
67
68/// A shell component that provides a consistent layout with a sidebar and a main content area.
69///
70/// This component is designed to wrap page-specific content, ensuring that the sidebar
71/// is always present for navigation. Integrates with [`crate::pages::sidebar::Sidebar`] for navigation.
72///
73/// # Mobile Support
74/// On mobile displays, the sidebar is hidden by default and can be toggled:
75/// - **Menu Toggle Button**: Renders a floating menu button to slide the sidebar open.
76/// - **Dark Mode Toggle**: Floating button to change theme.
77/// - **Overlay Backdrop**: Dims the screen when the sidebar is open. Clicking it closes the sidebar.
78/// - **Auto-Close on Navigation**: Automatically closes the sidebar when navigating to a new route.
79///
80/// # Components
81/// - [`crate::pages::sidebar::Sidebar`]: The navigation sidebar component, accepting mobile open state.
82/// - Main content area: Renders the `children` passed to this component.
83///
84/// # Example
85/// ```rust
86/// html! {
87///     <SidebarShell>
88///         <p>{"Your page content goes here."}</p>
89///     </SidebarShell>
90/// }
91/// ```
92#[component(SidebarShell)]
93fn sidebar_shell(props: &SidebarShellProps) -> Html {
94    let route = use_route::<Route>();
95    let mobile_sidebar_open = use_state(|| false);
96
97    // Close mobile sidebar automatically on any route transition
98    {
99        let mobile_sidebar_open = mobile_sidebar_open.clone();
100        use_effect_with(route, move |_| {
101            mobile_sidebar_open.set(false);
102            || ()
103        });
104    }
105
106    let on_open = {
107        let mobile_sidebar_open = mobile_sidebar_open.clone();
108        Callback::from(move |_: MouseEvent| mobile_sidebar_open.set(true))
109    };
110
111    let on_close = {
112        let mobile_sidebar_open = mobile_sidebar_open.clone();
113        Callback::from(move |_: ()| mobile_sidebar_open.set(false))
114    };
115
116    let on_close_click = {
117        let on_close = on_close.clone();
118        Callback::from(move |_: MouseEvent| on_close.emit(()))
119    };
120
121    html! {
122        <div class="layout">
123            <button class="mobile-menu-toggle" onclick={on_open}>
124                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
125                    <line x1="4" x2="20" y1="12" y2="12"/>
126                    <line x1="4" x2="20" y1="6" y2="6"/>
127                    <line x1="4" x2="20" y1="18" y2="18"/>
128                </svg>
129            </button>
130
131            <div class={if *mobile_sidebar_open { "sidebar-overlay open" } else { "sidebar-overlay" }} onclick={on_close_click}></div>
132
133            <sidebar::Sidebar is_open={*mobile_sidebar_open} on_close={on_close} />
134            <main class="content">
135                { for props.children.iter() }
136            </main>
137        </div>
138    }
139}
140
141
142/// Props for the AdminCheckWrapper component.
143#[derive(Properties, PartialEq)]
144pub struct AdminCheckWrapperProps {
145    pub children: Children,
146}
147
148/// Wrapper component that checks if an admin exists before rendering children.
149///
150/// This component is used to gate access to pages that should only be accessible before
151/// system initialization (e.g., login page). It performs an asynchronous check to the
152/// backend's `/api/check-admin` endpoint (via `crate::handlers::auth::check_admin_exists`) to determine system state.
153///
154/// # Behavior
155/// - **Loading**: Displays "Loading..." while checking admin status from the backend
156/// - **No Admin**: Automatically redirects to [`Route::Setup`] page for initialization
157/// - **Admin Exists**: Renders the wrapped children (e.g., login page)
158///
159/// # Example Usage
160/// ```ignore
161/// <AdminCheckWrapper>
162///     <LoginPage />
163/// </AdminCheckWrapper>
164/// ```
165///
166/// # Backend Integration
167/// The check queries the backend's `/api/check-admin` endpoint which returns `{"has_admin": bool}`.
168/// This allows the frontend to determine if initial admin setup is required.
169#[component(AdminCheckWrapper)]
170fn admin_check_wrapper(props: &AdminCheckWrapperProps) -> Html {
171    let admin_exists = use_state(|| None::<bool>);
172    let navigator = use_navigator().unwrap();
173
174    {
175        let admin_exists = admin_exists.clone();
176        use_effect_with((), move |_| {
177            let admin_exists = admin_exists.clone();
178            spawn_local(async move {
179                match Request::get("/api/check-admin").send().await {
180                    Ok(resp) if resp.status() == 200 => {
181                        if let Ok(data) = resp.json::<serde_json::Value>().await {
182                            let has_admin = data["has_admin"].as_bool().unwrap_or(false);
183                            admin_exists.set(Some(has_admin));
184                        }
185                    }
186                    _ => {
187                        admin_exists.set(Some(false));
188                    }
189                }
190            });
191            || ()
192        });
193    }
194
195    match *admin_exists {
196        None => html! { <div>{ "Wird geladen..." }</div> },
197        Some(false) => {
198            navigator.push(&Route::Setup);
199            html! { <div>{ "Wird weitergeleitet zur Einrichtung..." }</div> }
200        }
201        Some(true) => props.children.clone().into(),
202    }
203}
204
205/// The main routing logic for the application.
206///
207/// This function takes a [`Route`] enum variant and returns the corresponding HTML
208/// content to be rendered. It acts as a central dispatcher for the application's
209/// navigation and authentication flow.
210///
211/// Most routes are wrapped in [`ProtectedRoute`] to enforce authentication
212/// and authorization based on the `admin_page` flag, and in [`SidebarShell`] to maintain consistent layout.
213/// Login and Setup routes use [`AdminCheckWrapper`] instead to handle pre-authentication states.
214///
215/// # Arguments
216/// - `route`: The [`Route`] enum variant representing the current URL path.
217///
218/// # Returns
219/// An `Html` component that should be rendered for the given route.
220///
221/// # Route Protection
222/// - **Admin-required routes** (`admin_page={true}`): Require both authentication and admin privileges
223/// - **Public routes** (`admin_page={false}`): Require only authentication
224/// - **Pre-auth routes** (`AdminCheckWrapper`): Used before admin creation or login
225fn switch(route: Route) -> Html {
226    match route {
227        Route::Home => html! {
228            <ProtectedRoute admin_page={false}>
229                <SidebarShell>
230                    <basic_pages::Home/>
231                </SidebarShell>
232            </ProtectedRoute>
233        },
234        Route::NotFound => html! { <basic_pages::NotFound/> },
235        Route::Ticket => html! {
236            <ProtectedRoute admin_page={false}>
237                <SidebarShell>
238                    <ticket::SubmitTicket/>
239                </SidebarShell>
240            </ProtectedRoute>
241        },
242        Route::TicketById { id } => html! {
243            <ProtectedRoute admin_page={true}>
244                <SidebarShell>
245                    <ticket::TicketByID {id}/>
246                </SidebarShell>
247            </ProtectedRoute>
248        },
249        Route::AllTickets => html! {
250            <ProtectedRoute admin_page={false}>
251                <SidebarShell>
252                    <ticket::AllTickets/>
253                </SidebarShell>
254            </ProtectedRoute>
255        },
256        Route::ArchivedTickets => html! {
257            <ProtectedRoute admin_page={true}>
258                <SidebarShell>
259                    <ticket::ArchivedTickets/>
260                </SidebarShell>
261            </ProtectedRoute>
262        },
263        Route::Register => html! {
264            <ProtectedRoute admin_page={true}>
265                <SidebarShell>
266                    <user::Register/>
267                </SidebarShell>
268            </ProtectedRoute>
269        },
270        Route::Login => html! {
271            <AdminCheckWrapper>
272                <user::Login/>
273            </AdminCheckWrapper>
274        },
275        Route::Setup => html! {
276            <setup::InitialAdminSetup/>
277        },
278        Route::AllUsers => html! {
279            <ProtectedRoute admin_page={true}>
280                <SidebarShell>
281                    <user::AllUsers/>
282                </SidebarShell>
283            </ProtectedRoute>
284        },
285        Route::UserByID { id } => html! {
286            <ProtectedRoute admin_page={true}>
287                <SidebarShell>
288                    <user::UserByID {id}/>
289                </SidebarShell>
290            </ProtectedRoute>
291        },
292        Route::PermissionDenied => html! { <basic_pages::PermissionDenied/> },
293        Route::Diagnostics => html! {
294            <ProtectedRoute admin_page={true}>
295                <SidebarShell>
296                    <utilities::Diagnostics/>
297                </SidebarShell>
298            </ProtectedRoute>
299        },
300    }
301}
302
303/// The root component of the Yew application.
304///
305/// This component sets up the application's routing using `yew-router`'s
306/// `BrowserRouter` and `Switch` components. All other application content
307/// is rendered based on the current [`Route`] matched by the `switch` function.
308///
309/// Uses [`switch`] as the routing dispatcher to handle all route-specific rendering,
310/// which applies appropriate middleware like [`ProtectedRoute`] and [`AdminCheckWrapper`].
311///
312/// # Structure
313/// - `BrowserRouter`: Enables client-side routing.
314/// - `Switch`: Renders components based on the matched [`Route`].
315/// - `switch` function: Determines which component to render for each route.
316#[component(App)]
317pub fn app() -> Html {
318    html! {
319        <BrowserRouter>
320            <basic_pages::Icon/>
321            <Switch<Route> render={switch} />
322        </BrowserRouter>
323    }
324}