qorzen_oxide/ui/
router.rs1use 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#[derive(Clone, Routable, Debug, PartialEq)]
18#[rustfmt::skip]
19pub enum Route {
20 #[route("/login")]
22 Login {},
23
24 #[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 #[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 #[route("/:..segments")]
53 NotFound { segments: Vec<String> },
54}
55
56#[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#[component]
123fn AdminPageWithPermissionCheck() -> Element {
124 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#[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#[component]
221pub fn AuthenticatedLayout(children: Element) -> Element {
222 let app_state = use_app_state();
223 let navigator = use_navigator();
224
225 if let Some(_user) = &app_state.current_user {
227 rsx! {
228 Layout {
229 {children}
230 }
231 }
232 } else {
233 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#[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 let direct_permission = user.permissions.iter().any(|perm| {
265 (perm.resource == resource || perm.resource == "*")
266 && (perm.action == action || perm.action == "*")
267 });
268
269 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
304pub mod nav {
306 use super::*;
307
308 pub fn is_active_route(current: &Route, target: &Route) -> bool {
310 std::mem::discriminant(current) == std::mem::discriminant(target)
311 }
312
313 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 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}