actix-web-admin 0.1.0

A powerful admin panel library for Actix-web 4 applications
Documentation

actix-web-admin

A powerful admin panel library for Actix-web 4 applications that automatically generates CRUD interfaces.

Features

  • 🚀 Auto-generated CRUD operations - Create, Read, Update, Delete interfaces
  • 🔐 Built-in authentication - Simple session-based auth
  • 🎨 Modern responsive UI - Clean, professional admin interface
  • 🔍 Search & pagination - Built-in search and paginated lists
  • 📝 Form validation - Automatic form generation with validation
  • 🎯 Type-safe - Full Rust type safety with async trait support
  • 📦 Easy integration - Just implement one trait and you're done

Quick Start

Add to your Cargo.toml:

[dependencies]
actix-web-admin = "0.1.0"
actix-web = "4"
actix-session = { version = "0.9", features = ["cookie-session"] }
tokio = { version = "1", features = ["full"] }

Basic Example

use actix_web_admin::{AdminRegistry, AdminSite, AdminResource, init_templates};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

struct Product {
    id: String,
    name: String,
    price: f64,
}

struct ProductAdmin {
    db: Arc<Mutex<Vec<Product>>>,
}

#[async_trait]
impl AdminResource for ProductAdmin {
    fn name(&self) -> &str { "Product" }
    fn plural_name(&self) -> &str { "Products" }
    fn slug(&self) -> &str { "products" }
    fn icon(&self) -> &str { "📦" }

    fn list_columns(&self) -> Vec<Column> {
        vec![
            Column::text("name", "Name"),
            Column::number("price", "Price"),
        ]
    }

    fn form_fields(&self) -> Vec<FormField> {
        vec![
            FormField::text("name", "Name").required(),
            FormField::number("price", "Price").required(),
        ]
    }

    async fn list(&self, query: ListQuery) -> Result<ListResult, AdminError> {
        let db = self.db.lock().unwrap();
        let rows: Vec<serde_json::Value> = db.iter().map(|p| {
            serde_json::json!({
                "id": p.id,
                "name": p.name,
                "price": p.price,
            })
        }).collect();
        
        Ok(ListResult { 
            rows, 
            total: rows.len() as u64, 
            page: query.page.unwrap_or(1), 
            per_page: query.per_page.unwrap_or(10) 
        })
    }

    async fn get(&self, id: &str) -> Result<serde_json::Value, AdminError> {
        let db = self.db.lock().unwrap();
        db.iter()
            .find(|p| p.id == id)
            .map(|p| serde_json::json!({
                "id": p.id,
                "name": p.name,
                "price": p.price,
            }))
            .ok_or(AdminError::NotFound)
    }

    async fn create(&self, data: HashMap<String, serde_json::Value>) -> Result<serde_json::Value, AdminError> {
        let mut db = self.db.lock().unwrap();
        let id = uuid::Uuid::new_v4().to_string();
        let p = Product {
            id: id.clone(),
            name: data.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
            price: data.get("price").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0.0),
        };
        db.push(p);
        Ok(serde_json::json!({ "id": id }))
    }

    async fn update(&self, id: &str, data: HashMap<String, serde_json::Value>) -> Result<serde_json::Value, AdminError> {
        let mut db = self.db.lock().unwrap();
        if let Some(p) = db.iter_mut().find(|p| p.id == id) {
            if let Some(v) = data.get("name").and_then(|v| v.as_str()) { p.name = v.to_string(); }
            if let Some(v) = data.get("price").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()) { p.price = v; }
            return Ok(serde_json::json!({ "id": id }));
        }
        Err(AdminError::NotFound)
    }

    async fn delete(&self, id: &str) -> Result<(), AdminError> {
        let mut db = self.db.lock().unwrap();
        let len_before = db.len();
        db.retain(|p| p.id != id);
        if db.len() < len_before { Ok(()) } else { Err(AdminError::NotFound) }
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let templates = init_templates();
    let mut registry = AdminRegistry::new();
    registry.register(ProductAdmin {
        db: Arc::new(Mutex::new(vec![])),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(templates.clone()))
            .app_data(web::Data::new(actix_web_admin::handlers::auth::SimpleAuth {
                username: "admin".to_string(),
                password: "admin".to_string(),
            }))
            .wrap(SessionMiddleware::new(
                CookieSessionStore::default(), 
                actix_web::cookie::Key::generate()
            ))
            .configure(|cfg| {
                AdminSite::new("/admin").title("My Admin").mount(cfg, registry)
            })
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Run the example and visit http://localhost:8080/admin - login with admin/admin.

Configuration

AdminSite

AdminSite::new("/admin")  // URL prefix
    .title("My Admin Panel")  // Site title
    .mount(cfg, registry)     // Mount to Actix app

Resources

Each resource must implement the AdminResource trait:

  • name() - Singular display name
  • plural_name() - Plural display name
  • slug() - URL slug (must be unique)
  • icon() - Emoji icon for dashboard
  • list_columns() - Columns for list view
  • form_fields() - Fields for create/edit forms
  • CRUD operations: list(), get(), create(), update(), delete()

Field Types

FormField::text("name", "Name").required()
FormField::number("price", "Price")
FormField::boolean("active", "Active")
FormField::select("category", "Category", vec![
    ("electronics", "Electronics"),
    ("clothing", "Clothing"),
])
FormField::textarea("description", "Description", 4)

Column Types

Column::text("name", "Name")
Column::number("price", "Price")
Column::boolean("active", "Active")
Column::date("created_at", "Created")

Authentication

Built-in simple authentication:

app_data(web::Data::new(actix_web_admin::handlers::auth::SimpleAuth {
    username: "admin".to_string(),
    password: "admin".to_string(),
}))

Routes

/admin/                    # Dashboard
/admin/login              # Login page
/admin/logout             # Logout
/admin/{slug}/            # List view
/admin/{slug}/new         # Create form
/admin/{slug}/{id}        # Edit form
/admin/{slug}/{id}/delete # Delete action

Important Notes

Actix-Web Routing Behavior

This library follows Actix-web's routing philosophy. Some important considerations:

Slash Handling: Actix-web doesn't automatically handle URLs with and without trailing slashes. The library configures both:

// In site.rs - both routes are configured
.route("", web::get().to(handlers::dashboard::index))
.route("/", web::get().to(handlers::dashboard::index))

URL Construction: When constructing URLs in templates, use absolute paths to avoid double slashes:

// Instead of: format!("/{}/login", prefix.0)
// Use: "/admin/login"

Template Variables: Always provide default values in Tera templates:

{{ query.search | default(value='') }}
{{ query.sort_dir | default(value='asc') }}

Field Type Requirements

Some field types have additional required parameters:

// Textarea requires rows parameter
FormField::textarea("description", "Description", 4)  // rows = 4

Authentication

The library includes simple authentication by default:

.app_data(web::Data::new(actix_web_admin::handlers::auth::SimpleAuth {
    username: "admin".to_string(),
    password: "admin".to_string(),
}))

License

AGPL-3.0 - See LICENSE file for details.