AdminX

AdminX is a powerful, modern admin panel framework for Rust built on top of Actix Web and MongoDB. It provides a complete solution for creating administrative interfaces with minimal boilerplate code, featuring automatic CRUD operations, role-based access control, and a beautiful responsive UI.
β¨ Features
π Core Functionality
- Zero-Config CRUD Operations - Automatic Create, Read, Update, Delete with sensible defaults
- Schema-Driven Forms - Auto-generate forms from JSON Schema using
schemars
- Resource-Centric Architecture - Define resources once, get full admin interface
- Hybrid API/UI - Both REST API and web interface from same resource definitions
- Dynamic Menu Generation - Automatic navigation based on registered resources
π Security First
- JWT + Session Authentication - Secure token-based auth with session management
- Role-Based Access Control (RBAC) - Fine-grained permissions per resource
- Rate Limiting - Built-in protection against brute force attacks
- Timing Attack Prevention - Secure password verification
- CSRF Protection - Form-based submission security
π¨ Modern UI/UX
- Responsive Design - Mobile-first TailwindCSS-based interface
- Dark/Light Mode - Built-in theme switching
- Toast Notifications - User feedback with auto-dismiss
- Real-time Validation - Client-side form validation
- Accessibility - WCAG compliant with proper ARIA labels
π οΈ Developer Experience
- Minimal Boilerplate - Resources work out-of-the-box
- Type Safety - Full Rust type safety throughout
- Embedded Templates - Zero external dependencies
- Comprehensive Logging - Built-in tracing and debugging
- Hot Reload Support - Fast development iteration
π Quick Start
Add AdminX to your Cargo.toml:
[dependencies]
adminx = "0.1.0"
actix-web = "4"
mongodb = { version = "2.4", features = ["tokio-runtime"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
schemars = { version = "0.8", features = ["derive"] }
1. Define Collections
use actix_web::web;
use mongodb::{
bson::{doc, oid::ObjectId, to_bson, DateTime as BsonDateTime},
Collection, Database,
};
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
use serde_json::{self, Value};
use crate::services::redis_service::get_redis_connection;
use strum_macros::EnumIter;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, EnumIter)]
#[serde(rename_all = "lowercase")]
pub enum ImageStatus {
Active,
Inactive,
}
impl ToString for ImageStatus {
fn to_string(&self) -> String {
match self {
ImageStatus::Active => "active".to_string(),
ImageStatus::Inactive => "inactive".to_string(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Image {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
pub title: String,
pub image_url: String,
pub status: ImageStatus,
pub deleted: bool,
pub created_at: BsonDateTime,
pub updated_at: BsonDateTime,
}
impl Default for Image {
fn default() -> Self {
Self {
id: None,
title: String::from(""),
image_url: String::from(""),
status: ImageStatus::Active,
deleted: false,
created_at: BsonDateTime::now(),
updated_at: BsonDateTime::now(),
}
}
}
2. Define adminx initializer
use mongodb::Database;
use adminx::{
adminx_initialize,
get_adminx_config,
setup_adminx_logging,
get_adminx_session_middleware,
register_all_admix_routes,
registry::register_resource,
AdmixResource,
AdminxConfig,
};
use actix_session::SessionMiddleware;
use crate::admin::resources::image_resource::ImageResource;
pub struct AdminxInitializer;
impl AdminxInitializer {
pub async fn initialize(db: Database) -> AdminxConfig {
println!("Initializing AdminX components...");
let adminx_config = get_adminx_config();
setup_adminx_logging(&adminx_config);
let _adminx_instance = adminx_initialize(db.clone()).await;
Self::register_resources();
Self::print_debug_info();
adminx_config
}
fn register_resources() {
println!("π Registering AdminX resources...");
register_resource(Box::new(ImageResource::new()));
println!("All resources registered successfully!");
}
fn print_debug_info() {
let resources = adminx::registry::all_resources();
println!("π Total resources registered: {}", resources.len());
for resource in &resources {
println!(" - Resource: '{}' at path: '{}'",
resource.resource_name(),
resource.base_path());
}
}
pub fn get_session_middleware(config: &AdminxConfig) -> SessionMiddleware<impl actix_session::storage::SessionStore> {
get_adminx_session_middleware(config)
}
pub fn get_routes_service() -> actix_web::Scope {
register_all_admix_routes()
}
}
3. Define Resources
use crate::dbs::mongo::get_collection;
use adminx::{AdmixResource, error::AdminxError};
use async_trait::async_trait;
use mongodb::{Collection, bson::Document};
use serde_json::{json, Value};
use crate::models::image_model::ImageStatus;
use futures::future::BoxFuture;
use std::collections::HashMap;
use convert_case::{Case, Casing};
use strum::IntoEnumIterator;
pub struct ImageOptions;
impl ImageOptions {
pub fn statuses_options() -> Vec<Value> {
let mut options = vec![];
for variant in ImageStatus::iter() {
let value = serde_json::to_string(&variant).unwrap().replace('"', "");
let label = value.to_case(Case::Title);
options.push(json!({ "value": value, "label": label }));
}
options
}
pub fn boolean_options() -> Vec<Value> {
vec![
json!({ "value": "true", "label": "True" }),
json!({ "value": "false", "label": "False" }),
]
}
}
#[derive(Debug, Clone)]
pub struct ImageResource;
#[async_trait]
impl AdmixResource for ImageResource {
fn new() -> Self {
ImageResource
}
fn resource_name(&self) -> &'static str {
"Images"
}
fn base_path(&self) -> &'static str {
"images"
}
fn collection_name(&self) -> &'static str {
"images"
}
fn get_collection(&self) -> Collection<Document> {
get_collection::<Document>("images")
}
fn clone_box(&self) -> Box<dyn AdmixResource> {
Box::new(Self::new())
}
fn menu_group(&self) -> Option<&'static str> {
Some("Management")
}
fn menu(&self) -> &'static str {
"Images"
}
fn allowed_roles(&self) -> Vec<String> {
vec!["admin".to_string(), "superadmin".to_string()]
}
fn supports_file_upload(&self) -> bool {
true
}
fn max_file_size(&self) -> usize {
5 * 1024 * 1024 }
fn allowed_file_extensions(&self) -> Vec<&'static str> {
vec!["jpg", "jpeg", "png", "gif", "webp", "bmp", "pdf"]
}
fn permit_keys(&self) -> Vec<&'static str> {
vec!["title", "image_url", "status", "deleted"]
}
fn process_file_upload(&self, field_name: &str, file_data: &[u8], filename: &str) -> BoxFuture<'static, Result<HashMap<String, String>, AdminxError>> {
let filename = filename.to_string();
let field_name = field_name.to_string();
let file_data = file_data.to_vec();
let data_size = file_data.len();
Box::pin(async move {
tracing::info!("Processing file upload for field: {}, filename: {}, size: {} bytes",
field_name, filename, data_size);
let timestamp = chrono::Utc::now().timestamp();
let file_extension = filename.split('.').last().unwrap_or("jpg");
let unique_filename = format!("images/{}_{}.{}", timestamp, field_name, file_extension);
match crate::utils::s3_util::upload_image_to_s3(unique_filename.clone(), file_data).await {
Ok(public_url) => {
let mut urls = HashMap::new();
urls.insert("image_url".to_string(), public_url);
tracing::info!("File uploaded successfully to S3: {}", unique_filename);
Ok(urls)
}
Err(e) => {
tracing::error!("S3 upload failed for {}: {}", unique_filename, e);
Err(AdminxError::InternalError)
}
}
})
}
fn form_structure(&self) -> Option<Value> {
Some(json!({
"groups": [
{
"title": "Image Details",
"fields": [
{
"name": "title",
"field_type": "text",
"label": "Image Title",
"value": "",
"required": true,
"help_text": "Enter a descriptive title for the image"
},
{
"name": "image_file",
"field_type": "file",
"label": "Upload Image",
"accept": "image/*",
"required": true,
"help_text": "Upload an image file (JPG, PNG, GIF, WebP). Maximum size: 5MB."
},
{
"name": "status",
"field_type": "select",
"label": "Status",
"value": "active",
"required": true,
"options": ImageOptions::statuses_options(),
"help_text": "Set the image status"
},
{
"name": "deleted",
"field_type": "boolean",
"label": "Mark as Deleted",
"value": "false",
"required": false,
"options": ImageOptions::boolean_options(),
"help_text": "Mark this image as deleted (soft delete)"
}
]
}
]
}))
}
fn list_structure(&self) -> Option<Value> {
Some(json!({
"columns": [
{
"field": "title",
"label": "Title",
"sortable": true
},
{
"field": "image_url",
"label": "Image URL",
"sortable": false,
"type": "url"
},
{
"field": "status",
"label": "Status",
"sortable": true,
"type": "badge"
},
{
"field": "deleted",
"label": "Deleted",
"sortable": true,
"type": "boolean"
},
{
"field": "created_at",
"label": "Created At",
"type": "datetime",
"sortable": true
}
],
"actions": ["view", "edit", "delete"]
}))
}
fn view_structure(&self) -> Option<Value> {
Some(json!({
"sections": [
{
"title": "Image Information",
"fields": [
{
"field": "title",
"label": "Title"
},
{
"field": "image_url",
"label": "Image URL",
"type": "url"
},
{
"field": "status",
"label": "Status",
"type": "badge"
},
{
"field": "deleted",
"label": "Deleted",
"type": "boolean"
}
]
},
{
"title": "System Information",
"fields": [
{
"field": "_id",
"label": "Image ID"
},
{
"field": "created_at",
"label": "Created At",
"type": "datetime"
},
{
"field": "updated_at",
"label": "Updated At",
"type": "datetime"
}
]
}
]
}))
}
fn filters(&self) -> Option<Value> {
Some(json!({
"title": "Image Filters",
"filters": [
{
"field": "title",
"type": "text",
"label": "Title",
"placeholder": "Search by title..."
},
{
"field": "status",
"type": "select",
"label": "Status",
"options": ImageOptions::statuses_options(),
},
{
"field": "deleted",
"type": "boolean",
"label": "Show Deleted",
"options": ImageOptions::boolean_options(),
},
{
"field": "created_at",
"type": "date_range",
"label": "Created Date"
}
]
}))
}
fn custom_actions(&self) -> Vec<adminx::actions::CustomAction> {
vec![
adminx::actions::CustomAction {
name: "toggle_status",
method: "POST",
handler: |req, _path, _body| {
let image_id = req.match_info().get("id").unwrap_or("unknown").to_string();
Box::pin(async move {
tracing::info!("Toggling status for image: {}", image_id);
actix_web::HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": format!("Image {} status toggled", image_id)
}))
})
},
},
]
}
}
4. Set up Your Application
use actix_web::{web, App, HttpServer, middleware::Logger};
use dotenv::dotenv;
use std::env;
use crate::dbs::mongo::init_mongo_client;
use crate::admin::initializer::AdminxInitializer;
mod dbs;
mod admin;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
println!("Initializing database connection...");
let db = init_mongo_client().await;
let adminx_config = AdminxInitializer::initialize(db.clone()).await;
let server_address = env::var("SERVER_ADDRESS").unwrap_or_else(|_| "0.0.0.0:8080".to_string());
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(adminx_config.clone()))
.wrap(Logger::default())
.wrap(AdminxInitializer::get_session_middleware(&adminx_config))
.service(AdminxInitializer::get_routes_service())
})
.bind(server_address)?
.run()
.await
}
5. Environment Variables
Create a .env file:
JWT_SECRET=your-super-secret-jwt-key-minimum-32-characters
SESSION_SECRET=your-session-secret-key-must-be-at-least-64-characters-long
ENVIRONMENT=development
RUST_LOG=debug
5. Create admin username and password
cargo install adminx
export MONGODB_URL="mongodb://localhost:27017"
export ADMINX_DB_NAME="adminx"
adminx create -u admin -e admin@example.com -y
6. Start your application
cargo run
Visit `http://localhost:8080/adminx` and log in with credentails created in step 5:
π Documentation
Resource Customization
impl AdmixResource for UserResource {
fn create(&self, req: &HttpRequest, mut payload: Value) -> BoxFuture<'static, HttpResponse> {
let collection = self.get_collection();
Box::pin(async move {
if let Some(email) = payload.get("email").and_then(|e| e.as_str()) {
if email.is_empty() {
return HttpResponse::BadRequest().json(json!({
"error": "Email is required"
}));
}
}
payload["created_at"] = json!(mongodb::bson::DateTime::now());
})
}
fn filters(&self) -> Option<Value> {
Some(json!({
"filters": [
{"field": "name", "label": "Name", "type": "text"},
{"field": "email", "label": "Email", "type": "text"},
{"field": "age", "label": "Age", "type": "range"}
]
}))
}
fn custom_actions(&self) -> Vec<CustomAction> {
vec![
CustomAction {
name: "activate",
method: "POST",
handler: |_req, path, _body| {
Box::pin(async move {
let id = path.into_inner();
HttpResponse::Ok().json(json!({
"message": format!("User {} activated", id)
}))
})
}
}
]
}
}
Advanced Configuration
use adminx::middleware::RoleGuardMiddleware;
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(config.clone()))
.wrap(get_adminx_session_middleware(&config))
.wrap(Logger::default())
.route("/api/health", web::get().to(health_check))
.service(
web::scope("/admin")
.wrap(RoleGuard::admin_only())
.service(register_all_admix_routes())
)
.service(web::scope("/api").service(your_api_routes()))
})
Cli Configuration
Use environment variables
export MONGODB_URL="mongodb://localhost:27017"
export ADMINX_DB_NAME="adminx"
adminx create -u admin -e admin@example.com -y
Use command line arguments
adminx --mongodb-url "mongodb://localhost:27017" --database-name "adminx" list
Interactive mode (will prompt for connection details)
adminx create -u newuser -e user@example.com
Quick setup with defaults (localhost:27017, database: adminx)
adminx --mongodb-url "mongodb+srv://username:password@mongo-atlas-cluster.mongodb.net/?retryWrites=true&w=majority&appName=cluster-name" --database-name "dbname" create -u admin -e admin@srotas.space -p password -y
π― Examples
Check out the examples/ directory for complete working examples:
π§ Available Features
Resource Trait Methods
| Method |
Purpose |
Required |
resource_name() |
Display name |
β
|
base_path() |
URL path segment |
β
|
collection_name() |
MongoDB collection |
β
|
get_collection() |
Database connection |
β
|
clone_box() |
Resource cloning |
β
|
permit_params() |
Allowed fields |
βͺ |
allowed_roles() |
RBAC permissions |
βͺ |
form_structure() |
Custom forms |
βͺ |
list_structure() |
Table customization |
βͺ |
custom_actions() |
Additional endpoints |
βͺ |
Built-in Routes
Each registered resource automatically gets:
| Route |
Method |
Purpose |
/adminx/{resource}/list |
GET |
List view (HTML) |
/adminx/{resource}/new |
GET |
Create form (HTML) |
/adminx/{resource}/view/{id} |
GET |
Detail view (HTML) |
/adminx/{resource}/edit/{id} |
GET |
Edit form (HTML) |
/adminx/{resource}/create |
POST |
Create handler |
/adminx/{resource}/update/{id} |
POST |
Update handler |
/adminx/{resource} |
GET |
List API (JSON) |
/adminx/{resource} |
POST |
Create API (JSON) |
/adminx/{resource}/{id} |
GET |
Get API (JSON) |
/adminx/{resource}/{id} |
PUT |
Update API (JSON) |
/adminx/{resource}/{id} |
DELETE |
Delete API (JSON) |
π Security
AdminX includes comprehensive security features:
Authentication & Authorization
fn allowed_roles(&self) -> Vec<String> {
vec!["admin".to_string(), "moderator".to_string()]
}
fn allowed_roles_with_permissions(&self) -> Value {
json!({
"admin": ["create", "read", "update", "delete"],
"moderator": ["create", "read", "update"],
"user": ["read"]
})
}
Rate Limiting
Built-in rate limiting protects against brute force attacks:
if is_rate_limited(email, 5, Duration::from_secs(900)) {
return HttpResponse::TooManyRequests()
.body("Too many login attempts. Please try again later.");
}
π¨ UI Customization
Themes and Styling
AdminX uses TailwindCSS with built-in dark mode support:
<div class="flex gap-2">
<label><input type="radio" name="theme" value="light" onchange="setTheme(this.value)" /> Light</label>
<label><input type="radio" name="theme" value="dark" onchange="setTheme(this.value)" /> Dark</label>
</div>
Custom Templates
Override default templates by providing your own:
pub async fn render_custom_template(template_name: &str, ctx: Context) -> HttpResponse {
}
π§ͺ Testing
AdminX includes comprehensive test utilities:
#[cfg(test)]
mod tests {
use super::*;
use adminx::test_utils::*;
#[tokio::test]
async fn test_user_resource_crud() {
let resource = UserResource::new();
let test_db = setup_test_database().await;
let user_data = json!({
"name": "Test User",
"email": "test@example.com",
"age": 25
});
let response = resource.create(&test_request(), user_data).await;
assert!(response.status().is_success());
let response = resource.list(&test_request(), "".to_string()).await;
assert!(response.status().is_success());
}
}
π Performance
Database Optimization
impl AdmixResource for UserResource {
async fn setup_indexes(&self) -> Result<(), mongodb::error::Error> {
let collection = self.get_collection();
collection.create_index(
mongodb::IndexModel::builder()
.keys(doc! { "email": 1 })
.options(mongodb::options::IndexOptions::builder()
.unique(true)
.build())
.build(),
None
).await?;
Ok(())
}
}
Caching
fn cache_duration(&self) -> Option<Duration> {
Some(Duration::from_secs(300)) }
π€ Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Development Setup
git clone https://github.com/xsmmaurya/adminx.git
cd adminx
cargo build
cargo test
Running Examples
cd examples/basic-crud
cargo run
π License
This project is licensed under the MIT License - see the LICENSE file for details.
π Acknowledgments
- Built with Actix Web - Fast, powerful web framework
- UI powered by TailwindCSS - Utility-first CSS framework
- Templates with Tera - Jinja2-inspired template engine
- Database with MongoDB - Document database
- Schemas with Schemars - JSON Schema generation
π Support
Made with β€οΈ by the Rustacean360 Team
π₯ Contributors

πΊοΈ Roadmap
We are actively building AdminX step by step.
The roadmap includes phases like core CRUD foundation, extended resource features, authentication & RBAC, export/import, custom pages, UI themes, and optional extensions.
π See the full roadmap here: ROADMAP.md
