1use std::any::Any;
7use std::marker::PhantomData;
8
9use async_trait::async_trait;
10use bytes::Bytes;
11pub use cot_macros::AdminModel;
19use derive_more::Debug;
20use serde::Deserialize;
21
22use crate::auth::Auth;
23use crate::common_types::Password;
24use crate::error::NotFound;
25use crate::form::{
26 Form, FormContext, FormErrorTarget, FormField, FormFieldValidationError, FormResult,
27};
28use crate::html::Html;
29use crate::request::extractors::{FromRequestHead, Path, StaticFiles, UrlQuery};
30use crate::request::{Request, RequestExt, RequestHead};
31use crate::response::{IntoResponse, Response};
32use crate::router::{Router, Urls};
33use crate::static_files::StaticFile;
34use crate::{App, Error, Method, RequestHandler, Template, reverse_redirect};
35
36struct AdminAuthenticated<T, H: Send + Sync>(H, PhantomData<fn() -> T>);
37
38impl<T, H: RequestHandler<T> + Send + Sync> AdminAuthenticated<T, H> {
39 #[must_use]
40 fn new(handler: H) -> Self {
41 Self(handler, PhantomData)
42 }
43}
44
45impl<T, H: RequestHandler<T> + Send + Sync> RequestHandler<T> for AdminAuthenticated<T, H> {
46 async fn handle(&self, mut request: Request) -> crate::Result<Response> {
47 let auth: Auth = request.extract_from_head().await?;
48 if !auth.user().is_authenticated() {
49 return Ok(reverse_redirect!(request, "login")?);
50 }
51
52 self.0.handle(request).await
53 }
54}
55
56#[derive(Debug, FromRequestHead)]
57struct BaseContext {
58 urls: Urls,
59 static_files: StaticFiles,
60}
61
62async fn index(
63 base_context: BaseContext,
64 AdminModelManagers(managers): AdminModelManagers,
65) -> crate::Result<Html> {
66 #[derive(Debug, Template)]
67 #[template(path = "admin/model_list.html")]
68 struct ModelListTemplate<'a> {
69 ctx: &'a BaseContext,
70 #[debug("..")]
71 model_managers: Vec<Box<dyn AdminModelManager>>,
72 }
73
74 let template = ModelListTemplate {
75 ctx: &base_context,
76 model_managers: managers,
77 };
78 Ok(Html::new(template.render()?))
79}
80
81#[derive(Debug, Form)]
82struct LoginForm {
83 username: String,
84 password: Password,
85}
86
87async fn login(
88 base_context: BaseContext,
89 auth: Auth,
90 mut request: Request,
91) -> crate::Result<Response> {
92 #[derive(Debug, Template)]
93 #[template(path = "admin/login.html")]
94 struct LoginTemplate<'a> {
95 ctx: &'a BaseContext,
96 form: <LoginForm as Form>::Context,
97 }
98
99 let login_form_context = if request.method() == Method::GET {
100 LoginForm::build_context(&mut request).await?
101 } else if request.method() == Method::POST {
102 let login_form = LoginForm::from_request(&mut request).await?;
103 match login_form {
104 FormResult::Ok(login_form) => {
105 if authenticate(&auth, login_form).await? {
106 return Ok(reverse_redirect!(base_context.urls, "index")?);
107 }
108
109 let mut context = LoginForm::build_context(&mut request).await?;
110 context.add_error(
111 FormErrorTarget::Form,
112 FormFieldValidationError::from_static("Invalid username or password"),
113 );
114 context
115 }
116 FormResult::ValidationError(context) => context,
117 }
118 } else {
119 panic!("Unexpected request method");
120 };
121
122 let template = LoginTemplate {
123 ctx: &base_context,
124 form: login_form_context,
125 };
126 Html::new(template.render()?).into_response()
127}
128
129async fn authenticate(auth: &Auth, login_form: LoginForm) -> crate::Result<bool> {
130 #[cfg(feature = "db")]
131 let user = auth
132 .authenticate(&crate::auth::db::DatabaseUserCredentials::new(
133 login_form.username,
134 Password::new(login_form.password.into_string()),
135 ))
136 .await?;
137
138 #[cfg(not(feature = "db"))]
139 let user: Option<Box<dyn crate::auth::User + Send + Sync>> = None;
140
141 if let Some(user) = user {
142 auth.login(user).await?;
143 Ok(true)
144 } else {
145 Ok(false)
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
151pub struct Pagination {
152 limit: u64,
153 offset: u64,
154}
155
156impl Pagination {
157 fn new(limit: u64, page: u64) -> Self {
158 assert!(page > 0, "Page number must be greater than 0");
159
160 Self {
161 limit,
162 offset: (page - 1) * limit,
163 }
164 }
165
166 #[must_use]
168 pub fn limit(&self) -> u64 {
169 self.limit
170 }
171
172 #[must_use]
174 pub fn offset(&self) -> u64 {
175 self.offset
176 }
177}
178
179#[derive(Debug, Deserialize)]
180struct PaginationParams {
181 page: Option<u64>,
182 page_size: Option<u64>,
183}
184
185async fn view_model(
186 base_context: BaseContext,
187 managers: AdminModelManagers,
188 Path(model_name): Path<String>,
189 UrlQuery(pagination_params): UrlQuery<PaginationParams>,
190 request: Request,
191) -> crate::Result<Response> {
192 #[derive(Debug, Template)]
193 #[template(path = "admin/model.html")]
194 struct ModelTemplate<'a> {
195 ctx: &'a BaseContext,
196 #[debug("..")]
197 model: &'a dyn AdminModelManager,
198 #[debug("..")]
199 objects: Vec<Box<dyn AdminModel>>,
200 page: u64,
201 page_size: &'a u64,
202 total_object_counts: u64,
203 total_pages: u64,
204 }
205
206 const DEFAULT_PAGE_SIZE: u64 = 10;
207
208 let manager = get_manager(managers, &model_name)?;
209
210 let page = pagination_params.page.unwrap_or(1);
211 let page_size = pagination_params.page_size.unwrap_or(DEFAULT_PAGE_SIZE);
212
213 let total_object_counts = manager.get_total_object_counts(&request).await?;
214 let total_pages = total_object_counts.div_ceil(page_size);
215
216 if (page == 0 || page > total_pages) && total_pages > 0 {
217 return Err(Error::from(NotFound::with_message(format!(
218 "page {page} not found"
219 ))));
220 }
221
222 let pagination = Pagination::new(page_size, page);
223
224 let objects = manager.get_objects(&request, pagination).await?;
225
226 let template = ModelTemplate {
227 ctx: &base_context,
228 model: &*manager,
229 objects,
230 page,
231 page_size: &page_size,
232 total_object_counts,
233 total_pages,
234 };
235
236 Html::new(template.render()?).into_response()
237}
238
239async fn create_model_instance(
240 base_context: BaseContext,
241 managers: AdminModelManagers,
242 Path(model_name): Path<String>,
243 request: Request,
244) -> cot::Result<Response> {
245 edit_model_instance_impl(base_context, managers, request, &model_name, None).await
246}
247
248async fn edit_model_instance(
249 base_context: BaseContext,
250 managers: AdminModelManagers,
251 Path((model_name, object_id)): Path<(String, String)>,
252 request: Request,
253) -> cot::Result<Response> {
254 edit_model_instance_impl(
255 base_context,
256 managers,
257 request,
258 &model_name,
259 Some(&object_id),
260 )
261 .await
262}
263
264async fn edit_model_instance_impl(
265 base_context: BaseContext,
266 managers: AdminModelManagers,
267 mut request: Request,
268 model_name: &str,
269 object_id: Option<&str>,
270) -> cot::Result<Response> {
271 #[derive(Debug, Template)]
272 #[template(path = "admin/model_edit.html")]
273 struct ModelEditTemplate<'a> {
274 ctx: &'a BaseContext,
275 #[debug("..")]
276 model: &'a dyn AdminModelManager,
277 form_context: Box<dyn FormContext>,
278 is_edit: bool,
279 }
280
281 let manager = get_manager(managers, model_name)?;
282
283 let form_context = if request.method() == Method::POST {
284 let form_context = manager.save_from_request(&mut request, object_id).await?;
285
286 if let Some(form_context) = form_context {
287 form_context
288 } else {
289 return Ok(reverse_redirect!(
290 base_context.urls,
291 "view_model",
292 model_name = manager.url_name()
293 )?);
294 }
295 } else if let Some(object_id) = object_id {
296 let object = get_object(&mut request, &*manager, object_id).await?;
297
298 manager.form_context_from_object(object).await
299 } else {
300 manager.form_context()
301 };
302
303 let template = ModelEditTemplate {
304 ctx: &base_context,
305 model: &*manager,
306 form_context,
307 is_edit: object_id.is_some(),
308 };
309
310 Html::new(template.render()?).into_response()
311}
312
313async fn remove_model_instance(
314 base_context: BaseContext,
315 managers: AdminModelManagers,
316 Path((model_name, object_id)): Path<(String, String)>,
317 mut request: Request,
318) -> cot::Result<Response> {
319 #[derive(Debug, Template)]
320 #[template(path = "admin/model_remove.html")]
321 struct ModelRemoveTemplate<'a> {
322 ctx: &'a BaseContext,
323 #[debug("..")]
324 model: &'a dyn AdminModelManager,
325 #[debug("..")]
326 object: &'a dyn AdminModel,
327 }
328
329 let manager = get_manager(managers, &model_name)?;
330 let object = get_object(&mut request, &*manager, &object_id).await?;
331
332 if request.method() == Method::POST {
333 manager.remove_by_id(&mut request, &object_id).await?;
334
335 Ok(reverse_redirect!(
336 base_context.urls,
337 "view_model",
338 model_name = manager.url_name()
339 )?)
340 } else {
341 let template = ModelRemoveTemplate {
342 ctx: &base_context,
343 model: &*manager,
344 object: &*object,
345 };
346
347 Html::new(template.render()?).into_response()
348 }
349}
350
351async fn get_object(
352 request: &mut Request,
353 manager: &dyn AdminModelManager,
354 object_id: &str,
355) -> Result<Box<dyn AdminModel>, Error> {
356 manager
357 .get_object_by_id(request, object_id)
358 .await?
359 .ok_or_else(|| {
360 Error::from(NotFound::with_message(format!(
361 "Object with ID `{}` not found in model `{}`",
362 object_id,
363 manager.name()
364 )))
365 })
366}
367
368fn get_manager(
369 AdminModelManagers(model_managers): AdminModelManagers,
370 model_name: &str,
371) -> cot::Result<Box<dyn AdminModelManager>> {
372 model_managers
373 .into_iter()
374 .find(|manager| manager.url_name() == model_name)
375 .ok_or_else(|| {
376 Error::from(NotFound::with_message(format!(
377 "Model `{model_name}` not found"
378 )))
379 })
380}
381
382#[repr(transparent)]
383struct AdminModelManagers(Vec<Box<dyn AdminModelManager>>);
384
385impl FromRequestHead for AdminModelManagers {
386 async fn from_request_head(head: &RequestHead) -> cot::Result<Self> {
387 let managers = head
388 .context()
389 .apps()
390 .iter()
391 .flat_map(|app| app.admin_model_managers())
392 .collect();
393 Ok(Self(managers))
394 }
395}
396
397#[async_trait]
405pub trait AdminModelManager: Send + Sync {
406 fn name(&self) -> &str;
408
409 fn url_name(&self) -> &str;
411
412 async fn get_objects(
414 &self,
415 request: &Request,
416 pagination: Pagination,
417 ) -> cot::Result<Vec<Box<dyn AdminModel>>>;
418
419 async fn get_total_object_counts(&self, request: &Request) -> cot::Result<u64>;
421
422 async fn get_object_by_id(
424 &self,
425 request: &Request,
426 id: &str,
427 ) -> cot::Result<Option<Box<dyn AdminModel>>>;
428
429 fn form_context(&self) -> Box<dyn FormContext>;
431
432 async fn form_context_from_object(&self, object: Box<dyn AdminModel>) -> Box<dyn FormContext>;
440
441 async fn save_from_request(
448 &self,
449 request: &mut Request,
450 object_id: Option<&str>,
451 ) -> cot::Result<Option<Box<dyn FormContext>>>;
452
453 async fn remove_by_id(&self, request: &mut Request, object_id: &str) -> cot::Result<()>;
462}
463
464#[derive(Debug)]
466pub struct DefaultAdminModelManager<T> {
467 phantom_data: PhantomData<T>,
468}
469
470impl<T> Default for DefaultAdminModelManager<T> {
471 fn default() -> Self {
472 Self::new()
473 }
474}
475
476impl<T> DefaultAdminModelManager<T> {
477 #[must_use]
479 pub const fn new() -> Self {
480 Self {
481 phantom_data: PhantomData,
482 }
483 }
484}
485
486#[async_trait]
487impl<T: AdminModel + Send + Sync + 'static> AdminModelManager for DefaultAdminModelManager<T> {
488 fn name(&self) -> &str {
489 T::name()
490 }
491
492 fn url_name(&self) -> &str {
493 T::url_name()
494 }
495
496 async fn get_total_object_counts(&self, request: &Request) -> cot::Result<u64> {
497 T::get_total_object_counts(request).await
498 }
499
500 async fn get_objects(
501 &self,
502 request: &Request,
503 pagination: Pagination,
504 ) -> cot::Result<Vec<Box<dyn AdminModel>>> {
505 #[expect(trivial_casts)] T::get_objects(request, pagination).await.map(|objects| {
507 objects
508 .into_iter()
509 .map(|object| Box::new(object) as Box<dyn AdminModel>)
510 .collect()
511 })
512 }
513
514 async fn get_object_by_id(
515 &self,
516 request: &Request,
517 id: &str,
518 ) -> cot::Result<Option<Box<dyn AdminModel>>> {
519 #[expect(trivial_casts)] T::get_object_by_id(request, id)
521 .await
522 .map(|object| object.map(|object| Box::new(object) as Box<dyn AdminModel>))
523 }
524
525 fn form_context(&self) -> Box<dyn FormContext> {
526 T::form_context()
527 }
528
529 async fn form_context_from_object(&self, object: Box<dyn AdminModel>) -> Box<dyn FormContext> {
530 let object_any: &dyn Any = &*object;
531 let object_casted = object_any.downcast_ref::<T>().expect("Invalid object type");
532
533 T::form_context_from_self(object_casted).await
534 }
535
536 async fn save_from_request(
537 &self,
538 request: &mut Request,
539 object_id: Option<&str>,
540 ) -> cot::Result<Option<Box<dyn FormContext>>> {
541 T::save_from_request(request, object_id).await
542 }
543
544 async fn remove_by_id(&self, request: &mut Request, object_id: &str) -> cot::Result<()> {
545 T::remove_by_id(request, object_id).await
546 }
547}
548
549#[async_trait]
551#[diagnostic::on_unimplemented(
552 message = "`{Self}` does not implement the `AdminModel` trait",
553 label = "`{Self}` is not an admin model",
554 note = "add #[derive(cot::admin::AdminModel)] to the struct to automatically derive the trait"
555)]
556pub trait AdminModel: Any + Send + 'static {
557 async fn get_objects(request: &Request, pagination: Pagination) -> cot::Result<Vec<Self>>
559 where
560 Self: Sized;
561
562 async fn get_total_object_counts(request: &Request) -> cot::Result<u64>
564 where
565 Self: Sized;
566
567 async fn get_object_by_id(request: &Request, id: &str) -> cot::Result<Option<Self>>
569 where
570 Self: Sized;
571
572 fn name() -> &'static str
574 where
575 Self: Sized;
576
577 fn url_name() -> &'static str
579 where
580 Self: Sized;
581
582 fn id(&self) -> String;
584
585 fn display(&self) -> String;
587
588 fn form_context() -> Box<dyn FormContext>
590 where
591 Self: Sized;
592
593 async fn form_context_from_self(&self) -> Box<dyn FormContext>;
595
596 async fn save_from_request(
603 request: &mut Request,
604 object_id: Option<&str>,
605 ) -> cot::Result<Option<Box<dyn FormContext>>>
606 where
607 Self: Sized;
608
609 async fn remove_by_id(request: &mut Request, object_id: &str) -> cot::Result<()>
618 where
619 Self: Sized;
620}
621
622#[derive(Debug, Copy, Clone)]
639pub struct AdminApp;
640
641impl Default for AdminApp {
642 fn default() -> Self {
643 Self::new()
644 }
645}
646
647impl AdminApp {
648 #[must_use]
665 pub fn new() -> Self {
666 Self {}
667 }
668}
669
670impl App for AdminApp {
671 fn name(&self) -> &'static str {
672 "cot_admin"
673 }
674
675 fn router(&self) -> Router {
676 Router::with_urls([
677 crate::router::Route::with_handler_and_name(
678 "/",
679 AdminAuthenticated::new(index),
680 "index",
681 ),
682 crate::router::Route::with_handler_and_name("/login/", login, "login"),
683 crate::router::Route::with_handler_and_name(
684 "/{model_name}/",
685 AdminAuthenticated::new(view_model),
686 "view_model",
687 ),
688 crate::router::Route::with_handler_and_name(
689 "/{model_name}/create/",
690 AdminAuthenticated::new(create_model_instance),
691 "create_model_instance",
692 ),
693 crate::router::Route::with_handler_and_name(
694 "/{model_name}/{pk}/edit/",
695 AdminAuthenticated::new(edit_model_instance),
696 "edit_model_instance",
697 ),
698 crate::router::Route::with_handler_and_name(
699 "/{model_name}/{pk}/remove/",
700 AdminAuthenticated::new(remove_model_instance),
701 "remove_model_instance",
702 ),
703 ])
704 }
705
706 fn static_files(&self) -> Vec<StaticFile> {
707 vec![StaticFile::new(
708 "admin/admin.css",
709 Bytes::from_static(include_bytes!(concat!(
710 env!("OUT_DIR"),
711 "/static/admin/admin.css"
712 ))),
713 )]
714 }
715}