qorzen_oxide/ui/
router.rs

1// src/ui/router.rs - Routing configuration with authentication guards
2
3use dioxus::prelude::*;
4#[allow(unused_imports)]
5use dioxus_router::prelude::*;
6
7use crate::ui::{
8    layout::Layout,
9    pages::{
10        Dashboard as DashboardPage, Login as LoginPage, NotFound as NotFoundPage,
11        Plugins as PluginsPage, Profile as ProfilePage, Settings as SettingPage,
12    },
13    state::use_app_state,
14};
15
16/// Application routes with authentication and authorization
17#[derive(Clone, Routable, Debug, PartialEq)]
18#[rustfmt::skip]
19pub enum Route {
20    // Public routes
21    #[route("/login")]
22    Login {},
23
24    // Protected routes (require authentication)
25    #[route("/")]
26    #[redirect("/dashboard", || Route::Dashboard {})]
27    Home {},
28
29    #[route("/dashboard")]
30    Dashboard {},
31
32    #[route("/profile")]
33    Profile {},
34
35    #[route("/plugins")]
36    Plugins {},
37
38    #[route("/settings")]
39    Settings {},
40
41    #[route("/admin")]
42    Admin {},
43
44    // Plugin routes (dynamically loaded)
45    #[route("/plugin/:plugin_id")]
46    Plugin { plugin_id: String },
47
48    #[route("/plugin/:plugin_id/:page")]
49    PluginPage { plugin_id: String, page: String },
50
51    // Catch-all for 404
52    #[route("/:..segments")]
53    NotFound { segments: Vec<String> },
54}
55
56/// Route component implementations
57#[component]
58pub fn Login() -> Element {
59    rsx! {
60        div {
61            class: "min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8",
62            LoginPage {}
63        }
64    }
65}
66
67#[component]
68pub fn Home() -> Element {
69    rsx! {
70        AuthenticatedLayout {
71            DashboardPage {}
72        }
73    }
74}
75
76#[component]
77pub fn Dashboard() -> Element {
78    rsx! {
79        AuthenticatedLayout {
80            DashboardPage {}
81        }
82    }
83}
84
85#[component]
86pub fn Profile() -> Element {
87    rsx! {
88        AuthenticatedLayout {
89            ProfilePage {}
90        }
91    }
92}
93
94#[component]
95pub fn Plugins() -> Element {
96    rsx! {
97        AuthenticatedLayout {
98            PluginsPage {}
99        }
100    }
101}
102
103#[component]
104pub fn Settings() -> Element {
105    rsx! {
106        AuthenticatedLayout {
107            SettingPage {}
108        }
109    }
110}
111
112#[component]
113pub fn Admin() -> Element {
114    rsx! {
115        AuthenticatedLayout {
116            AdminPageWithPermissionCheck {}
117        }
118    }
119}
120
121/// Admin page with permission checking
122#[component]
123fn AdminPageWithPermissionCheck() -> Element {
124    // Check if user has admin permissions
125    let app_state = use_app_state();
126
127    match &app_state.current_user {
128        Some(user) => {
129            let has_admin_permission = user.roles.iter().any(|role| {
130                role.id == "admin"
131                    || role
132                        .permissions
133                        .iter()
134                        .any(|perm| perm.resource == "admin" && perm.action == "*")
135            });
136
137            if has_admin_permission {
138                rsx! {
139                    crate::ui::pages::Admin {}
140                }
141            } else {
142                rsx! {
143                    AccessDenied {}
144                }
145            }
146        }
147        None => {
148            rsx! {
149                AccessDenied {}
150            }
151        }
152    }
153}
154
155/// Access denied component
156#[component]
157fn AccessDenied() -> Element {
158    rsx! {
159        div {
160            class: "text-center py-12",
161            div {
162                class: "text-6xl text-red-500 mb-4",
163                "🚫"
164            }
165            h1 {
166                class: "text-2xl font-bold text-gray-900 mb-2",
167                "Access Denied"
168            }
169            p {
170                class: "text-gray-600 mb-6",
171                "You don't have permission to access this page."
172            }
173            Link {
174                to: Route::Dashboard {},
175                class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
176                "Go to Dashboard"
177            }
178        }
179    }
180}
181
182#[component]
183pub fn Plugin(plugin_id: String) -> Element {
184    rsx! {
185        AuthenticatedLayout {
186            crate::ui::pages::PluginView {
187                plugin_id: plugin_id
188            }
189        }
190    }
191}
192
193#[component]
194pub fn PluginPage(plugin_id: String, page: String) -> Element {
195    rsx! {
196        AuthenticatedLayout {
197            crate::ui::pages::PluginView {
198                plugin_id: plugin_id,
199                page: Some(page)
200            }
201        }
202    }
203}
204
205#[component]
206pub fn NotFound(segments: Vec<String>) -> Element {
207    let path = segments.join("/");
208
209    rsx! {
210        div {
211            class: "min-h-screen flex items-center justify-center bg-gray-50",
212            NotFoundPage {
213                path: path
214            }
215        }
216    }
217}
218
219/// Authenticated layout wrapper that checks authentication before rendering
220#[component]
221pub fn AuthenticatedLayout(children: Element) -> Element {
222    let app_state = use_app_state();
223    let navigator = use_navigator();
224
225    // Check if user is authenticated
226    if let Some(_user) = &app_state.current_user {
227        rsx! {
228            Layout {
229                {children}
230            }
231        }
232    } else {
233        // Redirect to login immediately (not in an effect)
234        navigator.push(Route::Login {});
235
236        rsx! {
237            div {
238                class: "min-h-screen flex items-center justify-center bg-gray-50",
239                div {
240                    class: "animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"
241                }
242                p {
243                    class: "mt-4 text-gray-600",
244                    "Redirecting to login..."
245                }
246            }
247        }
248    }
249}
250
251/// Permission guard component
252#[component]
253pub fn PermissionGuard(
254    resource: String,
255    action: String,
256    fallback: Option<Element>,
257    children: Element,
258) -> Element {
259    let app_state = use_app_state();
260
261    let has_permission = match &app_state.current_user {
262        Some(user) => {
263            // Check direct permissions
264            let direct_permission = user.permissions.iter().any(|perm| {
265                (perm.resource == resource || perm.resource == "*")
266                    && (perm.action == action || perm.action == "*")
267            });
268
269            // Check role permissions
270            let role_permission = user.roles.iter().any(|role| {
271                role.permissions.iter().any(|perm| {
272                    (perm.resource == resource || perm.resource == "*")
273                        && (perm.action == action || perm.action == "*")
274                })
275            });
276
277            direct_permission || role_permission
278        }
279        None => false,
280    };
281
282    if has_permission {
283        rsx! { {children} }
284    } else {
285        match fallback {
286            Some(fallback_element) => rsx! { {fallback_element} },
287            None => rsx! {
288                div {
289                    class: "text-center py-8",
290                    div {
291                        class: "text-4xl text-gray-400 mb-2",
292                        "🔒"
293                    }
294                    p {
295                        class: "text-gray-600",
296                        "Insufficient permissions"
297                    }
298                }
299            },
300        }
301    }
302}
303
304/// Navigation utilities
305pub mod nav {
306    use super::*;
307
308    /// Check if current route matches the given route
309    pub fn is_active_route(current: &Route, target: &Route) -> bool {
310        std::mem::discriminant(current) == std::mem::discriminant(target)
311    }
312
313    /// Get route title for display
314    pub fn route_title(route: &Route) -> &'static str {
315        match route {
316            Route::Login { .. } => "Login",
317            Route::Home { .. } => "Home",
318            Route::Dashboard { .. } => "Dashboard",
319            Route::Profile { .. } => "Profile",
320            Route::Plugins { .. } => "Plugins",
321            Route::Settings { .. } => "Settings",
322            Route::Admin { .. } => "Admin",
323            Route::Plugin { .. } => "Plugin",
324            Route::PluginPage { .. } => "Plugin Page",
325            Route::NotFound { .. } => "Not Found",
326        }
327    }
328
329    /// Get route icon (for navigation menus)
330    pub fn route_icon(route: &Route) -> &'static str {
331        match route {
332            Route::Login { .. } => "🔐",
333            Route::Home { .. } => "🏠",
334            Route::Dashboard { .. } => "📊",
335            Route::Profile { .. } => "👤",
336            Route::Plugins { .. } => "🧩",
337            Route::Settings { .. } => "⚙️",
338            Route::Admin { .. } => "👑",
339            Route::Plugin { .. } => "🔌",
340            Route::PluginPage { .. } => "📄",
341            Route::NotFound { .. } => "❓",
342        }
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_route_equality() {
352        let route1 = Route::Dashboard {};
353        let route2 = Route::Dashboard {};
354        assert_eq!(route1, route2);
355    }
356
357    #[test]
358    fn test_route_title() {
359        assert_eq!(nav::route_title(&Route::Dashboard {}), "Dashboard");
360        assert_eq!(nav::route_title(&Route::Profile {}), "Profile");
361    }
362
363    #[test]
364    fn test_route_icon() {
365        assert_eq!(nav::route_icon(&Route::Dashboard {}), "📊");
366        assert_eq!(nav::route_icon(&Route::Settings {}), "⚙️");
367    }
368}