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") .title("My Admin Panel") .mount(cfg, registry)
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:
.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:
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:
FormField::textarea("description", "Description", 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.