Skip to main content

actix_admin/
lib.rs

1//! # Actix Admin
2//!
3//! The actix-admin crate aims at creating a web admin interface similar to other admin interfaces (such as [flask-admin](https://github.com/flask-admin/flask-admin) in python).
4//!
5//! See the [documentation](https://mgugger.github.io/actix-admin/) at [https://mgugger.github.io/actix-admin/](https://mgugger.github.io/actix-admin/).
6
7use actix_session::Session;
8use actix_web::{
9    error,
10    http::{header::ContentType, StatusCode},
11    HttpResponse,
12};
13use async_trait::async_trait;
14use derive_more::{Display, Error};
15use sea_orm::DatabaseConnection;
16use serde_derive::Serialize;
17use std::collections::{BTreeMap, HashMap};
18use std::fmt::{self, Display as FmtDisplay};
19use tera::Tera;
20
21pub mod builder;
22pub mod csrf;
23pub mod model;
24pub mod routes;
25pub mod tera_templates;
26pub mod view_model;
27
28pub mod prelude {
29    pub use crate::builder::{ActixAdminBuilder, ActixAdminBuilderTrait};
30    pub use crate::csrf::{
31        csrf_token_for, verify_csrf, CsrfError, CSRF_HEADER, CSRF_QUERY_PARAM, CSRF_SESSION_KEY,
32    };
33    pub use crate::model::{
34        ActixAdminModel, ActixAdminModelFilter, ActixAdminModelFilterTrait,
35        ActixAdminModelFilterType, ActixAdminModelTrait, ActixAdminModelValidationTrait, FilterFn,
36    };
37    pub use crate::routes::{
38        bulk_action, create_or_edit_post, get_admin_ctx, ActixAdminBulkActionDispatch, SortOrder,
39    };
40    pub use crate::view_model::{
41        ActixAdminBulkAction, ActixAdminFilterOperator, ActixAdminPrimaryKey, ActixAdminViewModel,
42        ActixAdminViewModelField, ActixAdminViewModelFieldType, ActixAdminViewModelFilter,
43        ActixAdminViewModelParams, ActixAdminViewModelSerializable, ActixAdminViewModelTrait,
44    };
45    pub use crate::{hashmap, ActixAdminSelectListTrait};
46    pub use crate::{ActixAdmin, ActixAdminConfiguration, ActixAdminError, ActixAdminErrorType};
47    pub use actix_admin_macros::{
48        DeriveActixAdmin, DeriveActixAdminEnumSelectList, DeriveActixAdminModel,
49        DeriveActixAdminModelSelectList, DeriveActixAdminViewModel,
50    };
51    pub use actix_session::Session;
52    pub use async_trait::async_trait;
53}
54
55use crate::prelude::*;
56
57#[doc(hidden)]
58#[macro_export]
59macro_rules! hashmap {
60    ($( $key: expr => $val: expr ),*) => {{
61         let mut map = ::std::collections::HashMap::new();
62         $( map.insert($key.to_string(), $val); )*
63         map
64    }}
65}
66
67// SelectListTrait
68#[async_trait]
69pub trait ActixAdminSelectListTrait {
70    async fn get_key_value(
71        db: &DatabaseConnection,
72        tenant_ref: Option<i32>,
73    ) -> core::result::Result<Vec<(String, String)>, ActixAdminError>;
74}
75
76#[derive(Clone)]
77pub struct ActixAdminConfiguration {
78    pub enable_auth: bool,
79    pub user_is_logged_in: Option<for<'a> fn(&'a Session) -> bool>,
80    pub user_tenant_ref: Option<for<'a> fn(&'a Session) -> Option<i32>>,
81    pub login_link: Option<String>,
82    pub logout_link: Option<String>,
83    pub file_upload_directory: &'static str,
84    pub navbar_title: &'static str,
85    pub base_path: &'static str,
86    pub custom_css_paths: Option<Vec<String>>,
87    pub custom_js_paths: Option<Vec<String>>,
88    /// When `true` (default), every state-changing route (POST/DELETE/PUT) is
89    /// gated by a CSRF token stored in the actix-session cookie. Templates
90    /// automatically wire the token into every HTMX request as the
91    /// `X-CSRF-Token` header, and inject a hidden `_csrf` input into forms.
92    ///
93    /// Requires an `actix-session` middleware to be installed. If your admin
94    /// deployment is behind a non-cookie-session auth flow and you do not want
95    /// this protection (e.g. tests, an isolated intranet), set to `false`.
96    pub enable_csrf: bool,
97}
98
99impl Default for ActixAdminConfiguration {
100    fn default() -> Self {
101        Self {
102            enable_auth: false,
103            user_is_logged_in: None,
104            user_tenant_ref: None,
105            login_link: None,
106            logout_link: None,
107            file_upload_directory: "./file_uploads",
108            navbar_title: "Actix Admin",
109            base_path: "/admin",
110            custom_css_paths: None,
111            custom_js_paths: None,
112            enable_csrf: true,
113        }
114    }
115}
116
117#[derive(Clone)]
118pub struct ActixAdmin {
119    pub entity_names: BTreeMap<String, Vec<ActixAdminMenuElement>>,
120    pub view_models: HashMap<String, ActixAdminViewModel>,
121    pub card_grids: HashMap<String, Vec<Vec<String>>>,
122    pub configuration: ActixAdminConfiguration,
123    pub tera: Tera,
124    pub support_path: Option<String>,
125}
126
127#[derive(PartialEq, Eq, Clone, Serialize)]
128pub struct ActixAdminMenuElement {
129    pub name: String,
130    pub link: String,
131    pub is_custom_handler: bool,
132}
133
134#[derive(Debug, Error)]
135pub struct ActixAdminError {
136    pub ty: ActixAdminErrorType,
137    pub msg: String,
138}
139
140impl FmtDisplay for ActixAdminError {
141    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
142        write!(formatter, "{}: {}", self.ty, self.msg)
143    }
144}
145
146// Errors
147#[derive(Debug, Display, Error, PartialEq, Eq)]
148pub enum ActixAdminErrorType {
149    #[display("Internal error")]
150    InternalError,
151
152    #[display("Form has validation errors")]
153    ValidationErrors,
154
155    #[display("Bad request")]
156    BadRequest,
157
158    #[display("Unauthorized")]
159    Unauthorized,
160
161    #[display("Forbidden")]
162    Forbidden,
163
164    #[display("Could not list entities")]
165    ListError,
166
167    #[display("Could not create entity")]
168    CreateError,
169
170    #[display("Could not delete entity")]
171    DeleteError,
172
173    #[display("Could not edit entity")]
174    EditError,
175
176    #[display("Database error")]
177    DatabaseError,
178
179    #[display("Entity does not exist")]
180    EntityDoesNotExistError,
181
182    #[display("Upload error")]
183    UploadError,
184
185    #[display("IO error")]
186    IoError,
187
188    #[display("CSRF token missing or invalid")]
189    CsrfError,
190
191    #[display("Unknown bulk action")]
192    UnknownBulkAction,
193}
194
195impl ActixAdminError {
196    pub fn new(ty: ActixAdminErrorType, msg: impl Into<String>) -> Self {
197        Self {
198            ty,
199            msg: msg.into(),
200        }
201    }
202
203    pub fn bad_request(msg: impl Into<String>) -> Self {
204        Self::new(ActixAdminErrorType::BadRequest, msg)
205    }
206
207    pub fn not_found(msg: impl Into<String>) -> Self {
208        Self::new(ActixAdminErrorType::EntityDoesNotExistError, msg)
209    }
210
211    pub fn internal(msg: impl Into<String>) -> Self {
212        Self::new(ActixAdminErrorType::InternalError, msg)
213    }
214}
215
216impl error::ResponseError for ActixAdminError {
217    fn error_response(&self) -> HttpResponse {
218        HttpResponse::build(self.status_code())
219            .insert_header(ContentType::html())
220            .body(self.to_string())
221    }
222
223    fn status_code(&self) -> StatusCode {
224        use ActixAdminErrorType::*;
225        match self.ty {
226            BadRequest | ValidationErrors => StatusCode::BAD_REQUEST,
227            Unauthorized => StatusCode::UNAUTHORIZED,
228            Forbidden | CsrfError => StatusCode::FORBIDDEN,
229            EntityDoesNotExistError | UnknownBulkAction => StatusCode::NOT_FOUND,
230            InternalError | ListError | CreateError | DeleteError | EditError | DatabaseError
231            | UploadError | IoError => StatusCode::INTERNAL_SERVER_ERROR,
232        }
233    }
234}
235
236macro_rules! impl_from_error {
237    ($($err:ty => $ty:ident),* $(,)?) => {
238        $(
239            impl From<$err> for ActixAdminError {
240                fn from(err: $err) -> Self {
241                    Self { ty: ActixAdminErrorType::$ty, msg: err.to_string() }
242                }
243            }
244        )*
245    };
246}
247
248impl_from_error! {
249    sea_orm::DbErr => DatabaseError,
250    std::io::Error => IoError,
251    actix_multipart::MultipartError => UploadError,
252    serde_urlencoded::de::Error => BadRequest,
253}
254
255// Notifications
256#[derive(Debug, Display, Serialize)]
257pub enum ActixAdminNotificationType {
258    #[display("is-danger")]
259    Danger,
260}
261
262#[derive(Debug, Serialize)]
263pub struct ActixAdminNotification {
264    css_class: String,
265    message: String,
266}
267
268impl From<ActixAdminError> for ActixAdminNotification {
269    fn from(e: ActixAdminError) -> Self {
270        Self {
271            css_class: ActixAdminNotificationType::Danger.to_string(),
272            message: e.to_string(),
273        }
274    }
275}