1use crate::types::AdminError;
7use reinhardt_http::AuthState;
8use reinhardt_pages::server_fn::{ServerFnError, ServerFnRequest};
9use std::sync::Arc;
10
11pub trait IntoServerFnError {
13 fn into_server_fn_error(self) -> ServerFnError;
15}
16
17impl IntoServerFnError for AdminError {
18 fn into_server_fn_error(self) -> ServerFnError {
19 match self {
20 AdminError::ModelNotRegistered(msg) => ServerFnError::server(404, msg),
21 AdminError::PermissionDenied(msg) => ServerFnError::server(403, msg),
22 AdminError::InvalidAction(msg) | AdminError::ValidationError(msg) => {
23 ServerFnError::application(msg)
24 }
25 AdminError::DatabaseError(_) => {
26 ServerFnError::server(500, "Database operation failed")
28 }
29 AdminError::TemplateError(_) => {
30 ServerFnError::server(500, "Template rendering failed")
32 }
33 }
34 }
35}
36
37pub trait MapServerFnError<T> {
39 fn map_server_fn_error(self) -> Result<T, ServerFnError>;
41}
42
43impl<T> MapServerFnError<T> for Result<T, AdminError> {
44 fn map_server_fn_error(self) -> Result<T, ServerFnError> {
45 self.map_err(|e| e.into_server_fn_error())
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ModelPermission {
55 View,
57 Add,
59 Change,
61 Delete,
63}
64
65pub struct AdminAuth {
70 auth_state: Option<AuthState>,
72}
73
74impl AdminAuth {
75 pub fn from_request(request: &ServerFnRequest) -> Self {
85 let auth_state = request.inner().extensions.get::<AuthState>();
86 Self { auth_state }
87 }
88
89 pub fn from_arc_request(request: &Arc<reinhardt_http::Request>) -> Self {
99 let auth_state = request.extensions.get::<AuthState>();
100 Self { auth_state }
101 }
102
103 pub fn auth_state(&self) -> Option<&AuthState> {
105 self.auth_state.as_ref()
106 }
107
108 pub fn is_authenticated(&self) -> bool {
114 self.auth_state
115 .as_ref()
116 .is_some_and(|s| s.is_authenticated())
117 }
118
119 pub fn is_staff(&self) -> bool {
125 self.auth_state.as_ref().is_some_and(|s| s.is_admin())
126 }
127
128 pub fn is_active(&self) -> bool {
134 self.auth_state.as_ref().is_some_and(|s| s.is_active())
135 }
136
137 pub fn user_id(&self) -> Option<&str> {
139 self.auth_state.as_ref().map(|s| s.user_id())
140 }
141
142 pub fn require_authenticated(&self) -> Result<(), ServerFnError> {
148 if !self.is_authenticated() {
149 return Err(ServerFnError::server(
150 401,
151 "Authentication required to access admin panel",
152 ));
153 }
154 Ok(())
155 }
156
157 pub fn require_staff(&self) -> Result<(), ServerFnError> {
163 self.require_authenticated()?;
164 if !self.is_staff() {
165 return Err(ServerFnError::server(
166 403,
167 "Staff access required for admin panel",
168 ));
169 }
170 Ok(())
171 }
172
173 pub async fn require_model_permission(
196 &self,
197 model_admin: &dyn crate::core::ModelAdmin,
198 user: &dyn crate::core::AdminUser,
199 permission: ModelPermission,
200 ) -> Result<(), ServerFnError> {
201 self.require_staff()?;
202
203 let has_permission = match permission {
206 ModelPermission::View => model_admin.has_view_permission(user).await,
207 ModelPermission::Add => model_admin.has_add_permission(user).await,
208 ModelPermission::Change => model_admin.has_change_permission(user).await,
209 ModelPermission::Delete => model_admin.has_delete_permission(user).await,
210 };
211
212 if !has_permission {
213 return Err(ServerFnError::server(403, "Permission denied"));
214 }
215
216 Ok(())
217 }
218}
219
220#[cfg(all(test, server))]
221mod tests {
222 use super::*;
223 use async_trait::async_trait;
224 use rstest::rstest;
225 use std::sync::Arc;
226
227 struct TestUser;
231
232 impl crate::core::AdminUser for TestUser {
233 fn is_active(&self) -> bool {
234 true
235 }
236 fn is_staff(&self) -> bool {
237 true
238 }
239 fn is_superuser(&self) -> bool {
240 false
241 }
242 fn get_username(&self) -> &str {
243 "test_user"
244 }
245 }
246
247 struct DenyAllAdmin;
249
250 #[async_trait]
251 impl crate::core::ModelAdmin for DenyAllAdmin {
252 fn model_name(&self) -> &str {
253 "DenyModel"
254 }
255 }
256
257 struct AllowAllAdmin;
259
260 #[async_trait]
261 impl crate::core::ModelAdmin for AllowAllAdmin {
262 fn model_name(&self) -> &str {
263 "AllowModel"
264 }
265
266 async fn has_view_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
267 true
268 }
269 async fn has_add_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
270 true
271 }
272 async fn has_change_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
273 true
274 }
275 async fn has_delete_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
276 true
277 }
278 }
279
280 struct SelectiveAdmin {
282 allowed: ModelPermission,
283 }
284
285 #[async_trait]
286 impl crate::core::ModelAdmin for SelectiveAdmin {
287 fn model_name(&self) -> &str {
288 "SelectiveModel"
289 }
290
291 async fn has_view_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
292 self.allowed == ModelPermission::View
293 }
294 async fn has_add_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
295 self.allowed == ModelPermission::Add
296 }
297 async fn has_change_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
298 self.allowed == ModelPermission::Change
299 }
300 async fn has_delete_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
301 self.allowed == ModelPermission::Delete
302 }
303 }
304
305 fn make_admin_auth(auth_state: Option<AuthState>) -> AdminAuth {
307 let request = reinhardt_http::Request::builder()
308 .uri("/admin/test")
309 .build()
310 .expect("Failed to build test request");
311 if let Some(state) = auth_state {
312 request.extensions.insert(state);
313 }
314 AdminAuth::from_arc_request(&Arc::new(request))
315 }
316
317 #[rstest]
320 #[tokio::test]
321 async fn test_require_model_permission_staff_with_permission() {
322 let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
324 let admin = AllowAllAdmin;
325 let user_obj = TestUser;
326
327 let result = auth
329 .require_model_permission(
330 &admin,
331 &user_obj as &dyn crate::core::AdminUser,
332 ModelPermission::View,
333 )
334 .await;
335
336 assert!(result.is_ok());
338 }
339
340 #[rstest]
341 #[tokio::test]
342 async fn test_require_model_permission_staff_denied_by_model() {
343 let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
345 let admin = DenyAllAdmin;
346 let user_obj = TestUser;
347
348 let result = auth
350 .require_model_permission(
351 &admin,
352 &user_obj as &dyn crate::core::AdminUser,
353 ModelPermission::View,
354 )
355 .await;
356
357 assert!(result.is_err());
359 match result.unwrap_err() {
360 ServerFnError::Server { status, message } => {
361 assert_eq!(status, 403);
362 assert_eq!(message, "Permission denied");
363 }
364 other => panic!("Expected Server error with 403, got: {other:?}"),
365 }
366 }
367
368 #[rstest]
369 #[tokio::test]
370 async fn test_require_model_permission_non_staff_denied() {
371 let auth = make_admin_auth(Some(AuthState::authenticated("user1", false, true)));
373 let admin = AllowAllAdmin;
374 let user_obj = TestUser;
375
376 let result = auth
378 .require_model_permission(
379 &admin,
380 &user_obj as &dyn crate::core::AdminUser,
381 ModelPermission::View,
382 )
383 .await;
384
385 assert!(result.is_err());
387 match result.unwrap_err() {
388 ServerFnError::Server { status, message } => {
389 assert_eq!(status, 403);
390 assert_eq!(message, "Staff access required for admin panel");
391 }
392 other => panic!("Expected Server error with 403, got: {other:?}"),
393 }
394 }
395
396 #[rstest]
397 #[tokio::test]
398 async fn test_require_model_permission_unauthenticated() {
399 let auth = make_admin_auth(None);
401 let admin = AllowAllAdmin;
402 let user_obj = TestUser;
403
404 let result = auth
406 .require_model_permission(
407 &admin,
408 &user_obj as &dyn crate::core::AdminUser,
409 ModelPermission::View,
410 )
411 .await;
412
413 assert!(result.is_err());
415 match result.unwrap_err() {
416 ServerFnError::Server { status, message } => {
417 assert_eq!(status, 401);
418 assert_eq!(message, "Authentication required to access admin panel");
419 }
420 other => panic!("Expected Server error with 401, got: {other:?}"),
421 }
422 }
423
424 #[rstest]
425 #[case::view_matches_view(ModelPermission::View, ModelPermission::View, true)]
426 #[case::view_does_not_match_add(ModelPermission::View, ModelPermission::Add, false)]
427 #[case::add_matches_add(ModelPermission::Add, ModelPermission::Add, true)]
428 #[case::change_does_not_match_delete(ModelPermission::Change, ModelPermission::Delete, false)]
429 #[tokio::test]
430 async fn test_require_model_permission_selective_permissions(
431 #[case] granted: ModelPermission,
432 #[case] requested: ModelPermission,
433 #[case] expected_ok: bool,
434 ) {
435 let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
437 let admin = SelectiveAdmin { allowed: granted };
438 let user_obj = TestUser;
439
440 let result = auth
442 .require_model_permission(&admin, &user_obj as &dyn crate::core::AdminUser, requested)
443 .await;
444
445 assert_eq!(
447 result.is_ok(),
448 expected_ok,
449 "granted={granted:?}, requested={requested:?}: expected is_ok()={expected_ok}"
450 );
451 }
452
453 #[rstest]
456 #[test]
457 fn test_model_not_registered_converts_to_404() {
458 let admin_err = AdminError::ModelNotRegistered("User".into());
459 let server_err = admin_err.into_server_fn_error();
460
461 match server_err {
462 ServerFnError::Server { status, message } => {
463 assert_eq!(status, 404);
464 assert_eq!(message, "User");
465 }
466 _ => panic!("Expected Server error"),
467 }
468 }
469
470 #[rstest]
471 #[test]
472 fn test_permission_denied_converts_to_403() {
473 let admin_err = AdminError::PermissionDenied("Access denied".into());
474 let server_err = admin_err.into_server_fn_error();
475
476 match server_err {
477 ServerFnError::Server { status, message } => {
478 assert_eq!(status, 403);
479 assert_eq!(message, "Access denied");
480 }
481 _ => panic!("Expected Server error"),
482 }
483 }
484
485 #[rstest]
486 #[test]
487 fn test_validation_error_converts_to_application() {
488 let admin_err = AdminError::ValidationError("Invalid input".into());
489 let server_err = admin_err.into_server_fn_error();
490
491 match server_err {
492 ServerFnError::Application(msg) => {
493 assert_eq!(msg, "Invalid input");
494 }
495 _ => panic!("Expected Application error"),
496 }
497 }
498
499 #[rstest]
500 #[test]
501 fn test_database_error_hides_details() {
502 let admin_err = AdminError::DatabaseError("SQL syntax error at line 42".into());
503 let server_err = admin_err.into_server_fn_error();
504
505 match server_err {
506 ServerFnError::Server { status, message } => {
507 assert_eq!(status, 500);
508 assert_eq!(message, "Database operation failed");
509 assert!(!message.contains("SQL"));
511 assert!(!message.contains("42"));
512 }
513 _ => panic!("Expected Server error"),
514 }
515 }
516
517 #[rstest]
518 #[test]
519 fn test_result_conversion() {
520 let result: Result<String, AdminError> = Err(AdminError::ModelNotRegistered("Post".into()));
521 let server_result = result.map_server_fn_error();
522
523 assert!(server_result.is_err());
524 match server_result.unwrap_err() {
525 ServerFnError::Server { status, .. } => assert_eq!(status, 404),
526 _ => panic!("Expected Server error"),
527 }
528 }
529}