1use actix_session::Session;
2use serde_derive::Deserialize;
3use tera::Context;
4
5use crate::{prelude::*, ActixAdminNotification};
6use actix_web::{error, Error, HttpRequest, HttpResponse};
7
8use super::{Params, DEFAULT_ENTITIES_PER_PAGE};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum AdminAction {
13 View,
15 Create,
16 Edit,
17 Delete,
18 Export,
20 BulkAction,
22}
23
24pub struct RouteCtx<'a> {
29 pub actix_admin: &'a ActixAdmin,
30 pub view_model: &'a ActixAdminViewModel,
31 pub entity_name: String,
32 pub tenant_ref: Option<i32>,
33}
34
35#[derive(Clone, Copy)]
37pub struct RoutePrelude {
38 pub action: AdminAction,
39 pub verify_csrf: bool,
42 pub partial_unauth: bool,
45 pub with_auth_context: bool,
47}
48
49impl RoutePrelude {
50 pub const fn view() -> Self {
51 Self {
52 action: AdminAction::View,
53 verify_csrf: false,
54 partial_unauth: false,
55 with_auth_context: true,
56 }
57 }
58 pub const fn export() -> Self {
59 Self {
60 action: AdminAction::Export,
61 verify_csrf: false,
62 partial_unauth: false,
63 with_auth_context: false,
64 }
65 }
66 pub const fn create() -> Self {
67 Self {
68 action: AdminAction::Create,
69 verify_csrf: false,
70 partial_unauth: false,
71 with_auth_context: true,
72 }
73 }
74 pub const fn edit() -> Self {
75 Self {
76 action: AdminAction::Edit,
77 verify_csrf: false,
78 partial_unauth: false,
79 with_auth_context: true,
80 }
81 }
82 pub const fn write(action: AdminAction) -> Self {
83 Self {
84 action,
85 verify_csrf: true,
86 partial_unauth: true,
87 with_auth_context: false,
88 }
89 }
90 pub const fn bulk() -> Self {
91 Self {
92 action: AdminAction::BulkAction,
93 verify_csrf: true,
94 partial_unauth: false,
95 with_auth_context: false,
96 }
97 }
98}
99
100pub fn begin_route<'a, E: ActixAdminViewModelTrait>(
111 session: &Session,
112 req: &HttpRequest,
113 actix_admin: &'a ActixAdmin,
114 opts: RoutePrelude,
115) -> Result<Result<RouteCtx<'a>, HttpResponse>, Error> {
116 let entity_name = E::get_entity_name();
117 let view_model = view_model_or_500(actix_admin, &entity_name)?;
118
119 if !user_can_perform(session, actix_admin, view_model, opts.action) {
120 let mut ctx = Context::new();
121 if opts.with_auth_context {
122 add_auth_context(session, actix_admin, &mut ctx);
123 }
124 if opts.partial_unauth {
125 ctx.insert("render_partial", &true);
126 }
127 let resp = render_unauthorized(&ctx, actix_admin)?;
130 return Ok(Err(resp));
131 }
132
133 if opts.verify_csrf {
134 crate::csrf::verify_csrf(actix_admin, session, req)?;
135 }
136
137 let tenant_ref = actix_admin
138 .configuration
139 .user_tenant_ref
140 .and_then(|f| f(session));
141
142 Ok(Ok(RouteCtx {
143 actix_admin,
144 view_model,
145 entity_name,
146 tenant_ref,
147 }))
148}
149
150#[macro_export]
153macro_rules! admin_prelude {
154 ($session:expr, $req:expr, $actix_admin:expr, $opts:expr, $entity:ty) => {{
155 match $crate::routes::begin_route::<$entity>($session, $req, $actix_admin, $opts)? {
156 Ok(ctx) => ctx,
157 Err(resp) => return Ok(resp),
158 }
159 }};
160}
161
162pub fn add_auth_context(session: &Session, actix_admin: &ActixAdmin, ctx: &mut Context) {
163 let cfg = &actix_admin.configuration;
164 ctx.insert("enable_auth", &cfg.enable_auth);
165 ctx.insert("custom_css_paths", &cfg.custom_css_paths);
166 ctx.insert("custom_js_paths", &cfg.custom_js_paths);
167 ctx.insert("navbar_title", &cfg.navbar_title);
168 ctx.insert("base_path", &cfg.base_path);
169 ctx.insert("support_path", &actix_admin.support_path.as_ref());
170 ctx.insert("enable_csrf", &cfg.enable_csrf);
171 let mut token_value = String::new();
174 if cfg.enable_csrf {
175 token_value = csrf_token_for(session).unwrap_or_default();
176 }
177 ctx.insert("csrf_token", &token_value);
178 if cfg.enable_auth {
179 let func = cfg.user_is_logged_in.unwrap();
180 ctx.insert("user_is_logged_in", &func(session));
181 ctx.insert("login_link", cfg.login_link.as_deref().unwrap_or(""));
182 ctx.insert("logout_link", cfg.logout_link.as_deref().unwrap_or(""));
183 }
184}
185
186pub fn user_can_access_page(
187 session: &Session,
188 actix_admin: &ActixAdmin,
189 view_model: &ActixAdminViewModel,
190) -> bool {
191 let cfg = &actix_admin.configuration;
192 match (
193 cfg.enable_auth,
194 cfg.user_is_logged_in,
195 view_model.user_can_access,
196 ) {
197 (true, Some(auth), Some(vm_access)) => auth(session) && vm_access(session),
198 (true, Some(auth), None) => auth(session),
199 _ => !cfg.enable_auth,
200 }
201}
202
203pub fn user_can_perform(
206 session: &Session,
207 actix_admin: &ActixAdmin,
208 view_model: &ActixAdminViewModel,
209 action: AdminAction,
210) -> bool {
211 if !user_can_access_page(session, actix_admin, view_model) {
212 return false;
213 }
214 let hook = match action {
215 AdminAction::View => view_model.user_can_view_details,
216 AdminAction::Create => view_model.user_can_create,
217 AdminAction::Edit => view_model.user_can_edit,
218 AdminAction::Delete => view_model.user_can_delete,
219 AdminAction::Export => view_model.user_can_export,
220 AdminAction::BulkAction => return true,
223 };
224 match hook {
225 Some(f) => f(session),
226 None => true,
227 }
228}
229
230pub fn forbid_if_denied(
233 session: &Session,
234 actix_admin: &ActixAdmin,
235 view_model: &ActixAdminViewModel,
236 action: AdminAction,
237) -> Option<HttpResponse> {
238 if user_can_perform(session, actix_admin, view_model, action) {
239 None
240 } else {
241 Some(HttpResponse::Forbidden().finish())
242 }
243}
244
245pub fn render_unauthorized(ctx: &Context, actix_admin: &ActixAdmin) -> Result<HttpResponse, Error> {
246 let body = actix_admin
251 .tera
252 .render("unauthorized.html", ctx)
253 .unwrap_or_else(|_| String::from("Forbidden"));
254 Ok(HttpResponse::Forbidden()
255 .content_type("text/html")
256 .body(body))
257}
258
259pub fn render_template(
265 tera: &tera::Tera,
266 template_name: &str,
267 ctx: &Context,
268) -> Result<String, tera::Error> {
269 let render_partial = ctx
270 .get("render_partial")
271 .and_then(|v| v.as_bool())
272 .unwrap_or(false);
273 if render_partial {
274 tera.render_block(template_name, "content", ctx)
275 } else {
276 tera.render(template_name, ctx)
277 }
278}
279
280pub fn view_model_or_500<'a>(
283 actix_admin: &'a ActixAdmin,
284 entity_name: &str,
285) -> Result<&'a ActixAdminViewModel, Error> {
286 actix_admin.view_models.get(entity_name).ok_or_else(|| {
287 error::ErrorInternalServerError(format!(
288 "View model for entity '{entity_name}' is not registered"
289 ))
290 })
291}
292
293#[allow(clippy::too_many_arguments)]
298pub async fn render_create_or_edit_form<E: ActixAdminViewModelTrait>(
299 session: &Session,
300 req: HttpRequest,
301 actix_admin: &ActixAdmin,
302 view_model: &ActixAdminViewModel,
303 db: &sea_orm::DatabaseConnection,
304 entity_name: String,
305 model: &ActixAdminModel,
306 tenant_ref: Option<i32>,
307 notifications: Vec<ActixAdminNotification>,
308 is_inline: bool,
309 status: actix_web::http::StatusCode,
310) -> Result<HttpResponse, Error> {
311 let mut ctx = Context::new();
312 add_auth_context(session, actix_admin, &mut ctx);
313
314 let params = Params::from_query(req.query_string());
315 let search_params = SearchParams::from_params(¶ms, view_model);
316
317 ctx.insert(
318 "select_lists",
319 &E::get_select_lists(db, tenant_ref)
320 .await
321 .map_err(actix_web::error::ErrorInternalServerError)?,
322 );
323 ctx.insert("model", model);
324
325 add_default_context_with_session(
326 &mut ctx,
327 req,
328 view_model,
329 entity_name,
330 actix_admin,
331 notifications,
332 &search_params,
333 Some(session),
334 );
335
336 let template_path = if is_inline && model.primary_key.is_some() {
337 "create_or_edit/inline.html"
338 } else {
339 "create_or_edit.html"
340 };
341 let body = render_template(&actix_admin.tera, template_path, &ctx)
342 .map_err(actix_web::error::ErrorInternalServerError)?;
343 Ok(actix_web::HttpResponse::build(status)
344 .content_type("text/html")
345 .body(body))
346}
347
348pub fn validate_sort_by(view_model: &ActixAdminViewModel, sort_by: &str) -> Result<(), Error> {
351 if sort_by == view_model.primary_key {
352 return Ok(());
353 }
354 if view_model.fields.iter().any(|f| f.field_name == sort_by) {
355 Ok(())
356 } else {
357 Err(error::ErrorBadRequest(format!(
358 "Unknown sort column: {sort_by}"
359 )))
360 }
361}
362
363#[derive(Debug, Deserialize)]
364pub struct SearchParams {
365 pub page: u64,
366 pub entities_per_page: u64,
367 pub search: String,
368 pub sort_by: String,
369 pub sort_order: SortOrder,
370}
371
372impl SearchParams {
373 pub fn to_query_string(&self) -> String {
374 use urlencoding::encode;
375 format!(
376 "page={0}&search={1}&sort_by={2}&sort_order={3}&entities_per_page={4}",
377 self.page,
378 encode(&self.search),
379 encode(&self.sort_by),
380 self.sort_order,
381 self.entities_per_page,
382 )
383 }
384
385 pub fn from_params(params: &Params, view_model: &ActixAdminViewModel) -> Self {
386 SearchParams {
387 page: params.page.unwrap_or(1).max(1),
388 entities_per_page: params
389 .entities_per_page
390 .unwrap_or(DEFAULT_ENTITIES_PER_PAGE)
391 .max(1),
392 search: params.search.clone().unwrap_or_default(),
393 sort_by: params
394 .sort_by
395 .clone()
396 .unwrap_or_else(|| view_model.primary_key.clone()),
397 sort_order: params.sort_order.clone().unwrap_or(SortOrder::Asc),
398 }
399 }
400
401 pub fn from_list_query(q: &crate::routes::query::ListQuery) -> Self {
405 SearchParams {
406 page: q.page,
407 entities_per_page: q.entities_per_page,
408 search: q.search.clone(),
409 sort_by: q.sort_by.clone(),
410 sort_order: q.sort_order.clone(),
411 }
412 }
413}
414
415#[allow(dead_code)]
416pub fn add_default_context(
417 ctx: &mut Context,
418 req: HttpRequest,
419 view_model: &ActixAdminViewModel,
420 entity_name: String,
421 actix_admin: &ActixAdmin,
422 notifications: Vec<ActixAdminNotification>,
423 search_params: &SearchParams,
424) {
425 add_default_context_with_session(
426 ctx,
427 req,
428 view_model,
429 entity_name,
430 actix_admin,
431 notifications,
432 search_params,
433 None,
434 )
435}
436
437#[allow(clippy::too_many_arguments)]
440pub fn add_default_context_with_session(
441 ctx: &mut Context,
442 req: HttpRequest,
443 view_model: &ActixAdminViewModel,
444 entity_name: String,
445 actix_admin: &ActixAdmin,
446 notifications: Vec<ActixAdminNotification>,
447 search_params: &SearchParams,
448 session: Option<&Session>,
449) {
450 let render_partial = req.headers().contains_key("HX-Target");
451
452 let mut serializable = ActixAdminViewModelSerializable::from(view_model.clone());
453 if let Some(session) = session {
454 serializable.can_create =
455 user_can_perform(session, actix_admin, view_model, AdminAction::Create);
456 serializable.can_edit =
457 user_can_perform(session, actix_admin, view_model, AdminAction::Edit);
458 serializable.can_delete =
459 user_can_perform(session, actix_admin, view_model, AdminAction::Delete);
460 serializable.can_view_details =
461 user_can_perform(session, actix_admin, view_model, AdminAction::View);
462 serializable.can_export =
463 user_can_perform(session, actix_admin, view_model, AdminAction::Export);
464 }
465
466 ctx.insert("view_model", &serializable);
467 ctx.insert("entity_name", &entity_name);
468 ctx.insert("entity_names", &actix_admin.entity_names);
469 ctx.insert("notifications", ¬ifications);
470 ctx.insert("entities_per_page", &search_params.entities_per_page);
471 ctx.insert("render_partial", &render_partial);
472 ctx.insert("search", &search_params.search);
473 ctx.insert("sort_by", &search_params.sort_by);
474 ctx.insert("sort_order", &search_params.sort_order);
475 ctx.insert("page", &search_params.page);
476}