use crate::types::AdminError;
use reinhardt_http::AuthState;
use reinhardt_pages::server_fn::{ServerFnError, ServerFnRequest};
use std::sync::Arc;
pub trait IntoServerFnError {
fn into_server_fn_error(self) -> ServerFnError;
}
impl IntoServerFnError for AdminError {
fn into_server_fn_error(self) -> ServerFnError {
match self {
AdminError::ModelNotRegistered(msg) => ServerFnError::server(404, msg),
AdminError::PermissionDenied(msg) => ServerFnError::server(403, msg),
AdminError::InvalidAction(msg) | AdminError::ValidationError(msg) => {
ServerFnError::application(msg)
}
AdminError::DatabaseError(_) => {
ServerFnError::server(500, "Database operation failed")
}
AdminError::TemplateError(_) => {
ServerFnError::server(500, "Template rendering failed")
}
}
}
}
pub trait MapServerFnError<T> {
fn map_server_fn_error(self) -> Result<T, ServerFnError>;
}
impl<T> MapServerFnError<T> for Result<T, AdminError> {
fn map_server_fn_error(self) -> Result<T, ServerFnError> {
self.map_err(|e| e.into_server_fn_error())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelPermission {
View,
Add,
Change,
Delete,
}
pub struct AdminAuth {
auth_state: Option<AuthState>,
}
impl AdminAuth {
pub fn from_request(request: &ServerFnRequest) -> Self {
let auth_state = request.inner().extensions.get::<AuthState>();
Self { auth_state }
}
pub fn from_arc_request(request: &Arc<reinhardt_http::Request>) -> Self {
let auth_state = request.extensions.get::<AuthState>();
Self { auth_state }
}
pub fn auth_state(&self) -> Option<&AuthState> {
self.auth_state.as_ref()
}
pub fn is_authenticated(&self) -> bool {
self.auth_state
.as_ref()
.is_some_and(|s| s.is_authenticated())
}
pub fn is_staff(&self) -> bool {
self.auth_state.as_ref().is_some_and(|s| s.is_admin())
}
pub fn is_active(&self) -> bool {
self.auth_state.as_ref().is_some_and(|s| s.is_active())
}
pub fn user_id(&self) -> Option<&str> {
self.auth_state.as_ref().map(|s| s.user_id())
}
pub fn require_authenticated(&self) -> Result<(), ServerFnError> {
if !self.is_authenticated() {
return Err(ServerFnError::server(
401,
"Authentication required to access admin panel",
));
}
Ok(())
}
pub fn require_staff(&self) -> Result<(), ServerFnError> {
self.require_authenticated()?;
if !self.is_staff() {
return Err(ServerFnError::server(
403,
"Staff access required for admin panel",
));
}
Ok(())
}
pub async fn require_model_permission(
&self,
model_admin: &dyn crate::core::ModelAdmin,
user: &dyn crate::core::AdminUser,
permission: ModelPermission,
) -> Result<(), ServerFnError> {
self.require_staff()?;
let has_permission = match permission {
ModelPermission::View => model_admin.has_view_permission(user).await,
ModelPermission::Add => model_admin.has_add_permission(user).await,
ModelPermission::Change => model_admin.has_change_permission(user).await,
ModelPermission::Delete => model_admin.has_delete_permission(user).await,
};
if !has_permission {
return Err(ServerFnError::server(403, "Permission denied"));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use rstest::rstest;
use std::sync::Arc;
struct TestUser;
impl crate::core::AdminUser for TestUser {
fn is_active(&self) -> bool {
true
}
fn is_staff(&self) -> bool {
true
}
fn is_superuser(&self) -> bool {
false
}
fn get_username(&self) -> &str {
"test_user"
}
}
struct DenyAllAdmin;
#[async_trait]
impl crate::core::ModelAdmin for DenyAllAdmin {
fn model_name(&self) -> &str {
"DenyModel"
}
}
struct AllowAllAdmin;
#[async_trait]
impl crate::core::ModelAdmin for AllowAllAdmin {
fn model_name(&self) -> &str {
"AllowModel"
}
async fn has_view_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
true
}
async fn has_add_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
true
}
async fn has_change_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
true
}
async fn has_delete_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
true
}
}
struct SelectiveAdmin {
allowed: ModelPermission,
}
#[async_trait]
impl crate::core::ModelAdmin for SelectiveAdmin {
fn model_name(&self) -> &str {
"SelectiveModel"
}
async fn has_view_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
self.allowed == ModelPermission::View
}
async fn has_add_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
self.allowed == ModelPermission::Add
}
async fn has_change_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
self.allowed == ModelPermission::Change
}
async fn has_delete_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
self.allowed == ModelPermission::Delete
}
}
fn make_admin_auth(auth_state: Option<AuthState>) -> AdminAuth {
let request = reinhardt_http::Request::builder()
.uri("/admin/test")
.build()
.expect("Failed to build test request");
if let Some(state) = auth_state {
request.extensions.insert(state);
}
AdminAuth::from_arc_request(&Arc::new(request))
}
#[rstest]
#[tokio::test]
async fn test_require_model_permission_staff_with_permission() {
let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
let admin = AllowAllAdmin;
let user_obj = TestUser;
let result = auth
.require_model_permission(
&admin,
&user_obj as &dyn crate::core::AdminUser,
ModelPermission::View,
)
.await;
assert!(result.is_ok());
}
#[rstest]
#[tokio::test]
async fn test_require_model_permission_staff_denied_by_model() {
let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
let admin = DenyAllAdmin;
let user_obj = TestUser;
let result = auth
.require_model_permission(
&admin,
&user_obj as &dyn crate::core::AdminUser,
ModelPermission::View,
)
.await;
assert!(result.is_err());
match result.unwrap_err() {
ServerFnError::Server { status, message } => {
assert_eq!(status, 403);
assert_eq!(message, "Permission denied");
}
other => panic!("Expected Server error with 403, got: {other:?}"),
}
}
#[rstest]
#[tokio::test]
async fn test_require_model_permission_non_staff_denied() {
let auth = make_admin_auth(Some(AuthState::authenticated("user1", false, true)));
let admin = AllowAllAdmin;
let user_obj = TestUser;
let result = auth
.require_model_permission(
&admin,
&user_obj as &dyn crate::core::AdminUser,
ModelPermission::View,
)
.await;
assert!(result.is_err());
match result.unwrap_err() {
ServerFnError::Server { status, message } => {
assert_eq!(status, 403);
assert_eq!(message, "Staff access required for admin panel");
}
other => panic!("Expected Server error with 403, got: {other:?}"),
}
}
#[rstest]
#[tokio::test]
async fn test_require_model_permission_unauthenticated() {
let auth = make_admin_auth(None);
let admin = AllowAllAdmin;
let user_obj = TestUser;
let result = auth
.require_model_permission(
&admin,
&user_obj as &dyn crate::core::AdminUser,
ModelPermission::View,
)
.await;
assert!(result.is_err());
match result.unwrap_err() {
ServerFnError::Server { status, message } => {
assert_eq!(status, 401);
assert_eq!(message, "Authentication required to access admin panel");
}
other => panic!("Expected Server error with 401, got: {other:?}"),
}
}
#[rstest]
#[case::view_matches_view(ModelPermission::View, ModelPermission::View, true)]
#[case::view_does_not_match_add(ModelPermission::View, ModelPermission::Add, false)]
#[case::add_matches_add(ModelPermission::Add, ModelPermission::Add, true)]
#[case::change_does_not_match_delete(ModelPermission::Change, ModelPermission::Delete, false)]
#[tokio::test]
async fn test_require_model_permission_selective_permissions(
#[case] granted: ModelPermission,
#[case] requested: ModelPermission,
#[case] expected_ok: bool,
) {
let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
let admin = SelectiveAdmin { allowed: granted };
let user_obj = TestUser;
let result = auth
.require_model_permission(&admin, &user_obj as &dyn crate::core::AdminUser, requested)
.await;
assert_eq!(
result.is_ok(),
expected_ok,
"granted={granted:?}, requested={requested:?}: expected is_ok()={expected_ok}"
);
}
#[rstest]
#[test]
fn test_model_not_registered_converts_to_404() {
let admin_err = AdminError::ModelNotRegistered("User".into());
let server_err = admin_err.into_server_fn_error();
match server_err {
ServerFnError::Server { status, message } => {
assert_eq!(status, 404);
assert_eq!(message, "User");
}
_ => panic!("Expected Server error"),
}
}
#[rstest]
#[test]
fn test_permission_denied_converts_to_403() {
let admin_err = AdminError::PermissionDenied("Access denied".into());
let server_err = admin_err.into_server_fn_error();
match server_err {
ServerFnError::Server { status, message } => {
assert_eq!(status, 403);
assert_eq!(message, "Access denied");
}
_ => panic!("Expected Server error"),
}
}
#[rstest]
#[test]
fn test_validation_error_converts_to_application() {
let admin_err = AdminError::ValidationError("Invalid input".into());
let server_err = admin_err.into_server_fn_error();
match server_err {
ServerFnError::Application(msg) => {
assert_eq!(msg, "Invalid input");
}
_ => panic!("Expected Application error"),
}
}
#[rstest]
#[test]
fn test_database_error_hides_details() {
let admin_err = AdminError::DatabaseError("SQL syntax error at line 42".into());
let server_err = admin_err.into_server_fn_error();
match server_err {
ServerFnError::Server { status, message } => {
assert_eq!(status, 500);
assert_eq!(message, "Database operation failed");
assert!(!message.contains("SQL"));
assert!(!message.contains("42"));
}
_ => panic!("Expected Server error"),
}
}
#[rstest]
#[test]
fn test_result_conversion() {
let result: Result<String, AdminError> = Err(AdminError::ModelNotRegistered("Post".into()));
let server_result = result.map_server_fn_error();
assert!(server_result.is_err());
match server_result.unwrap_err() {
ServerFnError::Server { status, .. } => assert_eq!(status, 404),
_ => panic!("Expected Server error"),
}
}
}