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", ®istry.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", ®istry.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", ®istry.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", ®istry.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", ®istry.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}