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}