Skip to main content

actix_web_admin/handlers/
resource.rs

1use actix_session::Session;
2use actix_web::{web, HttpResponse, Responder};
3use std::collections::HashMap;
4use tera::{Context, Tera};
5use crate::registry::SharedRegistry;
6use crate::types::*;
7use crate::resource::{AdminTitle, AdminPrefix};
8use log::{info, error, warn};
9
10fn get_context(
11    session: &Session,
12    title: &AdminTitle,
13    _resource: &dyn crate::resource::AdminResource,
14    slug: &str,
15) -> Context {
16    let mut ctx = Context::new();
17    ctx.insert("title", &title.0);
18    let user = session.get::<String>("admin_user").ok().flatten().unwrap_or_else(|| "Guest".to_string());
19    ctx.insert("user", &user);
20    ctx.insert("current_slug", slug);
21    ctx
22}
23
24pub async fn list(
25    session: Session,
26    registry: web::Data<SharedRegistry>,
27    tmpl: web::Data<Tera>,
28    title: web::Data<AdminTitle>,
29    prefix: web::Data<AdminPrefix>,
30    path: web::Path<String>,
31    query: web::Query<ListQuery>,
32) -> impl Responder {
33    let slug = path.into_inner();
34    info!("Accessing list for slug: {}", slug);
35    
36    let user = match session.get::<String>("admin_user") {
37        Ok(Some(u)) => {
38            info!("User authenticated: {}", u);
39            u
40        },
41        _ => {
42            warn!("User not authenticated, redirecting to login");
43            return HttpResponse::Found().insert_header(("Location", format!("/{}/login", prefix.0))).finish();
44        }
45    };
46
47    let resource = match registry.get(&slug) {
48        Some(r) => {
49            info!("Resource found: {}", r.name());
50            r
51        },
52        None => {
53            error!("Resource not found for slug: {}", slug);
54            return HttpResponse::NotFound().finish();
55        }
56    };
57
58    let result = match resource.list(query.into_inner()).await {
59        Ok(res) => {
60            info!("List query successful, {} items", res.rows.len());
61            res
62        },
63        Err(e) => {
64            error!("List query failed: {}", e);
65            return HttpResponse::InternalServerError().body(e.to_string());
66        }
67    };
68
69    let mut ctx = get_context(&session, &title, &*resource, &slug);
70    ctx.insert("page_title", resource.plural_name());
71    ctx.insert("resource", &resource.info(&prefix.0));
72    ctx.insert("columns", &resource.list_columns());
73    ctx.insert("rows", &result.rows);
74    ctx.insert("page", &result.page);
75    ctx.insert("per_page", &result.per_page);
76    ctx.insert("total", &result.total);
77    ctx.insert("total_pages", &((result.total + result.per_page - 1) / result.per_page));
78    
79    let prefix_trimmed = prefix.0.trim_start_matches('/');
80    ctx.insert("path_dashboard", &format!("/{}/", prefix_trimmed));
81    ctx.insert("path_logout", &format!("/{}/logout", prefix_trimmed));
82    ctx.insert("user", &user);
83    
84    ctx.insert("resources", &registry.all().iter().map(|r| r.info(&prefix.0)).collect::<Vec<_>>());
85
86    match tmpl.render("list.html", &ctx) {
87        Ok(rendered) => {
88            info!("Template rendered successfully for list page");
89            HttpResponse::Ok().content_type("text/html").body(rendered)
90        },
91        Err(e) => {
92            error!("Template rendering failed: {}", e);
93            HttpResponse::InternalServerError().finish()
94        },
95    }
96}
97
98pub async fn new(
99    session: Session,
100    registry: web::Data<SharedRegistry>,
101    tmpl: web::Data<Tera>,
102    title: web::Data<AdminTitle>,
103    prefix: web::Data<AdminPrefix>,
104    path: web::Path<String>,
105) -> impl Responder {
106    let slug = path.into_inner();
107    let user = match session.get::<String>("admin_user") {
108        Ok(Some(u)) => u,
109        _ => return HttpResponse::Found().insert_header(("Location", format!("/{}/login", prefix.0))).finish(),
110    };
111
112    let resource = match registry.get(&slug) {
113        Some(r) => r,
114        None => return HttpResponse::NotFound().finish(),
115    };
116
117    let mut ctx = get_context(&session, &title, &*resource, &slug);
118    ctx.insert("page_title", "New Record");
119    ctx.insert("resource", &resource.info(&prefix.0));
120    ctx.insert("fields", &resource.form_fields());
121    ctx.insert("values", &HashMap::<String, serde_json::Value>::new());
122    ctx.insert("errors", &HashMap::<String, String>::new());
123    ctx.insert("is_new", &true);
124    ctx.insert("resources", &registry.all().iter().map(|r| r.info(&prefix.0)).collect::<Vec<_>>());
125    let prefix_trimmed = prefix.0.trim_start_matches('/');
126    ctx.insert("path_dashboard", &format!("/{}/", prefix_trimmed));
127    ctx.insert("path_logout", &format!("/{}/logout", prefix_trimmed));
128    ctx.insert("user", &user);
129
130    match tmpl.render("form.html", &ctx) {
131        Ok(rendered) => HttpResponse::Ok().content_type("text/html").body(rendered),
132        Err(_) => HttpResponse::InternalServerError().finish(),
133    }
134}
135
136pub async fn create(
137    session: Session,
138    registry: web::Data<SharedRegistry>,
139    tmpl: web::Data<Tera>,
140    title: web::Data<AdminTitle>,
141    prefix: web::Data<AdminPrefix>,
142    path: web::Path<String>,
143    form: web::Form<HashMap<String, String>>,
144) -> impl Responder {
145    let slug = path.into_inner();
146    let user = match session.get::<String>("admin_user") {
147        Ok(Some(u)) => u,
148        _ => return HttpResponse::Found().insert_header(("Location", format!("/{}/login", prefix.0))).finish(),
149    };
150
151    let resource = match registry.get(&slug) {
152        Some(r) => r,
153        None => return HttpResponse::NotFound().finish(),
154    };
155
156    let mut data = HashMap::new();
157    for (k, v) in form.into_inner() {
158        data.insert(k, serde_json::Value::String(v));
159    }
160
161    match resource.validate(&data).await {
162        Ok(_) => {
163            match resource.create(data).await {
164                Ok(_) => HttpResponse::Found().insert_header(("Location", format!("/{}/{}/", prefix.0, slug))).finish(),
165                Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
166            }
167        }
168        Err(errors) => {
169            let mut ctx = get_context(&session, &title, &*resource, &slug);
170            ctx.insert("page_title", "New Record");
171            ctx.insert("resource", &resource.info(&prefix.0));
172            ctx.insert("fields", &resource.form_fields());
173            ctx.insert("values", &data);
174            ctx.insert("errors", &errors);
175            ctx.insert("is_new", &true);
176            ctx.insert("resources", &registry.all().iter().map(|r| r.info(&prefix.0)).collect::<Vec<_>>());
177            ctx.insert("path_dashboard", &format!("/{}/", prefix.0));
178            ctx.insert("path_logout", &format!("/{}/logout", prefix.0));
179            ctx.insert("user", &user);
180            
181            match tmpl.render("form.html", &ctx) {
182                Ok(rendered) => HttpResponse::BadRequest().content_type("text/html").body(rendered),
183                Err(_) => HttpResponse::InternalServerError().finish(),
184            }
185        }
186    }
187}
188
189pub async fn edit(
190    session: Session,
191    registry: web::Data<SharedRegistry>,
192    tmpl: web::Data<Tera>,
193    title: web::Data<AdminTitle>,
194    prefix: web::Data<AdminPrefix>,
195    path: web::Path<(String, String)>,
196) -> impl Responder {
197    let (slug, id) = path.into_inner();
198    let user = match session.get::<String>("admin_user") {
199        Ok(Some(u)) => u,
200        _ => return HttpResponse::Found().insert_header(("Location", format!("/{}/login", prefix.0))).finish(),
201    };
202
203    let resource = match registry.get(&slug) {
204        Some(r) => r,
205        None => return HttpResponse::NotFound().finish(),
206    };
207
208    let val = match resource.get(&id).await {
209        Ok(v) => v,
210        Err(AdminError::NotFound) => return HttpResponse::NotFound().finish(),
211        Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
212    };
213
214    let mut values = HashMap::new();
215    if let Some(obj) = val.as_object() {
216        for (k, v) in obj {
217            values.insert(k.clone(), v.clone());
218        }
219    }
220
221    let mut ctx = get_context(&session, &title, &*resource, &slug);
222    ctx.insert("page_title", "Edit Record");
223    ctx.insert("resource", &resource.info(&prefix.0));
224    ctx.insert("fields", &resource.form_fields());
225    ctx.insert("values", &values);
226    ctx.insert("errors", &HashMap::<String, String>::new());
227    ctx.insert("is_new", &false);
228    ctx.insert("resources", &registry.all().iter().map(|r| r.info(&prefix.0)).collect::<Vec<_>>());
229    let prefix_trimmed = prefix.0.trim_start_matches('/');
230    ctx.insert("path_dashboard", &format!("/{}/", prefix_trimmed));
231    ctx.insert("path_logout", &format!("/{}/logout", prefix_trimmed));
232    ctx.insert("user", &user);
233
234    match tmpl.render("form.html", &ctx) {
235        Ok(rendered) => HttpResponse::Ok().content_type("text/html").body(rendered),
236        Err(_) => HttpResponse::InternalServerError().finish(),
237    }
238}
239
240pub async fn update(
241    session: Session,
242    registry: web::Data<SharedRegistry>,
243    tmpl: web::Data<Tera>,
244    title: web::Data<AdminTitle>,
245    prefix: web::Data<AdminPrefix>,
246    path: web::Path<(String, String)>,
247    form: web::Form<HashMap<String, String>>,
248) -> impl Responder {
249    let (slug, id) = path.into_inner();
250    let user = match session.get::<String>("admin_user") {
251        Ok(Some(u)) => u,
252        _ => return HttpResponse::Found().insert_header(("Location", format!("/{}/login", prefix.0))).finish(),
253    };
254
255    let resource = match registry.get(&slug) {
256        Some(r) => r,
257        None => return HttpResponse::NotFound().finish(),
258    };
259
260    let mut data = HashMap::new();
261    for (k, v) in form.into_inner() {
262        data.insert(k, serde_json::Value::String(v));
263    }
264
265    match resource.validate(&data).await {
266        Ok(_) => {
267            match resource.update(&id, data).await {
268                Ok(_) => HttpResponse::Found().insert_header(("Location", format!("/{}/{}/", prefix.0, slug))).finish(),
269                Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
270            }
271        }
272        Err(errors) => {
273            let mut ctx = get_context(&session, &title, &*resource, &slug);
274            ctx.insert("page_title", "Edit Record");
275            ctx.insert("resource", &resource.info(&prefix.0));
276            ctx.insert("fields", &resource.form_fields());
277            ctx.insert("values", &data);
278            ctx.insert("errors", &errors);
279            ctx.insert("is_new", &false);
280            ctx.insert("resources", &registry.all().iter().map(|r| r.info(&prefix.0)).collect::<Vec<_>>());
281            ctx.insert("path_dashboard", &format!("/{}/", prefix.0));
282            ctx.insert("path_logout", &format!("/{}/logout", prefix.0));
283            ctx.insert("user", &user);
284            
285            match tmpl.render("form.html", &ctx) {
286                Ok(rendered) => HttpResponse::BadRequest().content_type("text/html").body(rendered),
287                Err(_) => HttpResponse::InternalServerError().finish(),
288            }
289        }
290    }
291}
292
293pub async fn delete(
294    session: Session,
295    registry: web::Data<SharedRegistry>,
296    prefix: web::Data<AdminPrefix>,
297    path: web::Path<(String, String)>,
298) -> impl Responder {
299    let (slug, id) = path.into_inner();
300    let _user = match session.get::<String>("admin_user") {
301        Ok(Some(u)) => u,
302        _ => return HttpResponse::Found().insert_header(("Location", format!("/{}/login", prefix.0))).finish(),
303    };
304
305    let resource = match registry.get(&slug) {
306        Some(r) => r,
307        None => return HttpResponse::NotFound().finish(),
308    };
309
310    match resource.delete(&id).await {
311        Ok(_) => HttpResponse::Found().insert_header(("Location", format!("/{}/{}/", prefix.0, slug))).finish(),
312        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
313    }
314}