The docs now include dark mode, the sidebar on mobile and a expanded ticket lifecycle
324 lines
11 KiB
Rust
324 lines
11 KiB
Rust
mod auth;
|
|
mod pages;
|
|
mod dark_mode;
|
|
use crate::auth::ProtectedRoute;
|
|
use crate::pages::*;
|
|
use gloo_net::http::Request;
|
|
use wasm_bindgen_futures::spawn_local;
|
|
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
|
|
/// Defines the application's various routes and their corresponding paths.
|
|
///
|
|
/// This enum is used by `yew-router` to map URLs to specific components,
|
|
/// enabling navigation within the single-page application. Each route is protected
|
|
/// by [`ProtectedRoute`] middleware where appropriate to enforce authentication and authorization.
|
|
/// See [`switch`] for the routing logic.
|
|
#[derive(Clone, PartialEq, Routable)]
|
|
enum Route {
|
|
/// The application's home page.
|
|
#[at("/")]
|
|
Home,
|
|
/// Route for submitting a new ticket.
|
|
#[at("/ticket")]
|
|
Ticket,
|
|
/// Route for viewing a specific ticket by its ID.
|
|
#[at("/tickets/:id")]
|
|
TicketById { id: i32 },
|
|
/// Route for viewing all tickets.
|
|
#[at("/tickets")]
|
|
AllTickets,
|
|
/// Route for viewing archived tickets.
|
|
#[at("/tickets/archive")]
|
|
ArchivedTickets,
|
|
/// Route for user registration.
|
|
#[at("/register")]
|
|
Register,
|
|
/// Route for user login.
|
|
#[at("/login")]
|
|
Login,
|
|
/// Route for the initial administrator setup.
|
|
#[at("/setup")]
|
|
Setup,
|
|
/// Route for viewing all users.
|
|
#[at("/users")]
|
|
AllUsers,
|
|
/// Route for viewing a specific user by their ID.
|
|
#[at("/users/:id")]
|
|
UserByID { id: i16 },
|
|
/// Route for displaying diagnostics information (admin-only).
|
|
#[at("/diagnostics")]
|
|
Diagnostics,
|
|
/// Route displayed when a user attempts to access a page without sufficient permissions.
|
|
#[at("/denied")]
|
|
PermissionDenied,
|
|
/// Catch-all route for unmatched paths, leading to a 404 Not Found page.
|
|
#[not_found]
|
|
#[at("/404")]
|
|
NotFound,
|
|
}
|
|
|
|
/// Properties for the [`SidebarShell`] component.
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct SidebarShellProps {
|
|
/// The child components to be rendered within the main content area of the shell.
|
|
pub children: Children,
|
|
}
|
|
|
|
/// A shell component that provides a consistent layout with a sidebar and a main content area.
|
|
///
|
|
/// This component is designed to wrap page-specific content, ensuring that the sidebar
|
|
/// is always present for navigation. Integrates with [`crate::pages::sidebar::Sidebar`] for navigation.
|
|
///
|
|
/// # Mobile Support
|
|
/// On mobile displays, the sidebar is hidden by default and can be toggled:
|
|
/// - **Menu Toggle Button**: Renders a floating menu button to slide the sidebar open.
|
|
/// - **Dark Mode Toggle**: Floating button to change theme.
|
|
/// - **Overlay Backdrop**: Dims the screen when the sidebar is open. Clicking it closes the sidebar.
|
|
/// - **Auto-Close on Navigation**: Automatically closes the sidebar when navigating to a new route.
|
|
///
|
|
/// # Components
|
|
/// - [`crate::pages::sidebar::Sidebar`]: The navigation sidebar component, accepting mobile open state.
|
|
/// - Main content area: Renders the `children` passed to this component.
|
|
///
|
|
/// # Example
|
|
/// ```rust
|
|
/// html! {
|
|
/// <SidebarShell>
|
|
/// <p>{"Your page content goes here."}</p>
|
|
/// </SidebarShell>
|
|
/// }
|
|
/// ```
|
|
#[component(SidebarShell)]
|
|
fn sidebar_shell(props: &SidebarShellProps) -> Html {
|
|
let route = use_route::<Route>();
|
|
let mobile_sidebar_open = use_state(|| false);
|
|
|
|
// Close mobile sidebar automatically on any route transition
|
|
{
|
|
let mobile_sidebar_open = mobile_sidebar_open.clone();
|
|
use_effect_with(route, move |_| {
|
|
mobile_sidebar_open.set(false);
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
let on_open = {
|
|
let mobile_sidebar_open = mobile_sidebar_open.clone();
|
|
Callback::from(move |_: MouseEvent| mobile_sidebar_open.set(true))
|
|
};
|
|
|
|
let on_close = {
|
|
let mobile_sidebar_open = mobile_sidebar_open.clone();
|
|
Callback::from(move |_: ()| mobile_sidebar_open.set(false))
|
|
};
|
|
|
|
let on_close_click = {
|
|
let on_close = on_close.clone();
|
|
Callback::from(move |_: MouseEvent| on_close.emit(()))
|
|
};
|
|
|
|
html! {
|
|
<div class="layout">
|
|
<button class="mobile-menu-toggle" onclick={on_open}>
|
|
<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">
|
|
<line x1="4" x2="20" y1="12" y2="12"/>
|
|
<line x1="4" x2="20" y1="6" y2="6"/>
|
|
<line x1="4" x2="20" y1="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<div class={if *mobile_sidebar_open { "sidebar-overlay open" } else { "sidebar-overlay" }} onclick={on_close_click}></div>
|
|
|
|
<sidebar::Sidebar is_open={*mobile_sidebar_open} on_close={on_close} />
|
|
<main class="content">
|
|
{ for props.children.iter() }
|
|
</main>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
/// Props for the AdminCheckWrapper component.
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct AdminCheckWrapperProps {
|
|
pub children: Children,
|
|
}
|
|
|
|
/// Wrapper component that checks if an admin exists before rendering children.
|
|
///
|
|
/// This component is used to gate access to pages that should only be accessible before
|
|
/// system initialization (e.g., login page). It performs an asynchronous check to the
|
|
/// backend's `/api/check-admin` endpoint (via `crate::handlers::auth::check_admin_exists`) to determine system state.
|
|
///
|
|
/// # Behavior
|
|
/// - **Loading**: Displays "Loading..." while checking admin status from the backend
|
|
/// - **No Admin**: Automatically redirects to [`Route::Setup`] page for initialization
|
|
/// - **Admin Exists**: Renders the wrapped children (e.g., login page)
|
|
///
|
|
/// # Example Usage
|
|
/// ```ignore
|
|
/// <AdminCheckWrapper>
|
|
/// <LoginPage />
|
|
/// </AdminCheckWrapper>
|
|
/// ```
|
|
///
|
|
/// # Backend Integration
|
|
/// The check queries the backend's `/api/check-admin` endpoint which returns `{"has_admin": bool}`.
|
|
/// This allows the frontend to determine if initial admin setup is required.
|
|
#[component(AdminCheckWrapper)]
|
|
fn admin_check_wrapper(props: &AdminCheckWrapperProps) -> Html {
|
|
let admin_exists = use_state(|| None::<bool>);
|
|
let navigator = use_navigator().unwrap();
|
|
|
|
{
|
|
let admin_exists = admin_exists.clone();
|
|
use_effect_with((), move |_| {
|
|
let admin_exists = admin_exists.clone();
|
|
spawn_local(async move {
|
|
match Request::get("/api/check-admin").send().await {
|
|
Ok(resp) if resp.status() == 200 => {
|
|
if let Ok(data) = resp.json::<serde_json::Value>().await {
|
|
let has_admin = data["has_admin"].as_bool().unwrap_or(false);
|
|
admin_exists.set(Some(has_admin));
|
|
}
|
|
}
|
|
_ => {
|
|
admin_exists.set(Some(false));
|
|
}
|
|
}
|
|
});
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
match *admin_exists {
|
|
None => html! { <div>{ "Lade..." }</div> },
|
|
Some(false) => {
|
|
navigator.push(&Route::Setup);
|
|
html! { <div>{ "Leite weiter zur Einrichtung..." }</div> }
|
|
}
|
|
Some(true) => props.children.clone().into(),
|
|
}
|
|
}
|
|
|
|
/// The main routing logic for the application.
|
|
///
|
|
/// This function takes a [`Route`] enum variant and returns the corresponding HTML
|
|
/// content to be rendered. It acts as a central dispatcher for the application's
|
|
/// navigation and authentication flow.
|
|
///
|
|
/// Most routes are wrapped in [`ProtectedRoute`] to enforce authentication
|
|
/// and authorization based on the `admin_page` flag, and in [`SidebarShell`] to maintain consistent layout.
|
|
/// Login and Setup routes use [`AdminCheckWrapper`] instead to handle pre-authentication states.
|
|
///
|
|
/// # Arguments
|
|
/// - `route`: The [`Route`] enum variant representing the current URL path.
|
|
///
|
|
/// # Returns
|
|
/// An `Html` component that should be rendered for the given route.
|
|
///
|
|
/// # Route Protection
|
|
/// - **Admin-required routes** (`admin_page={true}`): Require both authentication and admin privileges
|
|
/// - **Public routes** (`admin_page={false}`): Require only authentication
|
|
/// - **Pre-auth routes** (`AdminCheckWrapper`): Used before admin creation or login
|
|
fn switch(route: Route) -> Html {
|
|
match route {
|
|
Route::Home => html! {
|
|
<ProtectedRoute admin_page={false}>
|
|
<SidebarShell>
|
|
<basic_pages::Home/>
|
|
</SidebarShell>
|
|
</ProtectedRoute>
|
|
},
|
|
Route::NotFound => html! { <basic_pages::NotFound/> },
|
|
Route::Ticket => html! {
|
|
<ProtectedRoute admin_page={false}>
|
|
<SidebarShell>
|
|
<ticket::SubmitTicket/>
|
|
</SidebarShell>
|
|
</ProtectedRoute>
|
|
},
|
|
Route::TicketById { id } => html! {
|
|
<ProtectedRoute admin_page={true}>
|
|
<SidebarShell>
|
|
<ticket::TicketByID {id}/>
|
|
</SidebarShell>
|
|
</ProtectedRoute>
|
|
},
|
|
Route::AllTickets => html! {
|
|
<ProtectedRoute admin_page={false}>
|
|
<SidebarShell>
|
|
<ticket::AllTickets/>
|
|
</SidebarShell>
|
|
</ProtectedRoute>
|
|
},
|
|
Route::ArchivedTickets => html! {
|
|
<ProtectedRoute admin_page={true}>
|
|
<SidebarShell>
|
|
<ticket::ArchivedTickets/>
|
|
</SidebarShell>
|
|
</ProtectedRoute>
|
|
},
|
|
Route::Register => html! {
|
|
<ProtectedRoute admin_page={true}>
|
|
<SidebarShell>
|
|
<user::Register/>
|
|
</SidebarShell>
|
|
</ProtectedRoute>
|
|
},
|
|
Route::Login => html! {
|
|
<AdminCheckWrapper>
|
|
<user::Login/>
|
|
</AdminCheckWrapper>
|
|
},
|
|
Route::Setup => html! {
|
|
<setup::InitialAdminSetup/>
|
|
},
|
|
Route::AllUsers => html! {
|
|
<ProtectedRoute admin_page={true}>
|
|
<SidebarShell>
|
|
<user::AllUsers/>
|
|
</SidebarShell>
|
|
</ProtectedRoute>
|
|
},
|
|
Route::UserByID { id } => html! {
|
|
<ProtectedRoute admin_page={true}>
|
|
<SidebarShell>
|
|
<user::UserByID {id}/>
|
|
</SidebarShell>
|
|
</ProtectedRoute>
|
|
},
|
|
Route::PermissionDenied => html! { <basic_pages::PermissionDenied/> },
|
|
Route::Diagnostics => html! {
|
|
<ProtectedRoute admin_page={true}>
|
|
<SidebarShell>
|
|
<utilities::Diagnostics/>
|
|
</SidebarShell>
|
|
</ProtectedRoute>
|
|
},
|
|
}
|
|
}
|
|
|
|
/// The root component of the Yew application.
|
|
///
|
|
/// This component sets up the application's routing using `yew-router`'s
|
|
/// `BrowserRouter` and `Switch` components. All other application content
|
|
/// is rendered based on the current [`Route`] matched by the `switch` function.
|
|
///
|
|
/// Uses [`switch`] as the routing dispatcher to handle all route-specific rendering,
|
|
/// which applies appropriate middleware like [`ProtectedRoute`] and [`AdminCheckWrapper`].
|
|
///
|
|
/// # Structure
|
|
/// - `BrowserRouter`: Enables client-side routing.
|
|
/// - `Switch`: Renders components based on the matched [`Route`].
|
|
/// - `switch` function: Determines which component to render for each route.
|
|
#[component(App)]
|
|
pub fn app() -> Html {
|
|
html! {
|
|
<BrowserRouter>
|
|
<dark_mode::DarkModeToggle />
|
|
<Switch<Route> render={switch} />
|
|
</BrowserRouter>
|
|
}
|
|
}
|