1use std::sync::Arc;
5
6use toasty::{Db, schema::Model};
7use topcoat::{
8 Result,
9 asset::AssetConfig,
10 context::Cx,
11 cookie::RouterBuilderCookieExt,
12 router::{
13 Body, LayoutFn, Path, RouteFn, RouteFuture, RouterBuilder, RouterBuilderDirectoryExt,
14 error::redirect,
15 },
16 runtime::{PrefetchMode, RouterBuilderRuntimeExt, RuntimeSetup},
17 session::{RouterBuilderSessionExt, SessionConfig},
18};
19
20use super::{
21 Panel, Root,
22 forms::MAX_FORM_BYTES,
23 headers,
24 register::Registry,
25 state::{PanelState, Panels, current, under_prefix},
26};
27use crate::{
28 DeclarationError, DeclarationErrorKind, MountError, Site,
29 auth::{PanelGate, RuntimeGate, SESSION_LIFETIME},
30 declaration::segment_fault,
31 form::RecordForm,
32 policy::Ability,
33 resource::{MountScope, Mounted, Mounts, Resource, require_mounted},
34 tenancy::TenantSource,
35 topcoat_compat::RUNTIME_PREFIX,
36};
37
38pub trait RouterBuilderPanelExt: Sized {
52 fn panel(self, panel: Panel) -> Result<Self>;
54}
55
56impl RouterBuilderPanelExt for RouterBuilder {
57 fn panel(self, panel: Panel) -> Result<Self> {
58 panel.mount(self)
59 }
60}
61
62impl Panel {
63 pub fn context(self, db: &Db) -> Result<Cx> {
76 let Panel {
77 prefix,
78 registrations,
79 configuration_errors,
80 ..
81 } = self;
82 let mut registry = Registry::new(prefix.clone(), Some(db.schema().clone()));
83 let (mounts, mut errors) = registry.register_all(registrations, configuration_errors);
84 let cx = registry.check_all(db, &mounts, &mut errors);
85 if !errors.is_empty() {
86 return Err(MountError::new(&prefix, errors).into());
87 }
88 Ok(cx)
89 }
90
91 fn mount(self, mut builder: RouterBuilder) -> Result<RouterBuilder> {
92 let db = builder.get_app_context::<Db>().cloned();
93 let mut registry = Registry::new(
94 self.prefix.clone(),
95 db.as_ref().map(|db| db.schema().clone()),
96 );
97 let Panel {
98 prefix,
99 shell_assets,
100 brand,
101 dark_mode,
102 layout,
103 registrations,
104 frame_ancestors,
105 configuration_errors,
106 uploads,
107 served_dirs,
108 login_hint,
109 auth,
110 } = self;
111 let (mounts, mut errors) = registry.register_all(registrations, configuration_errors);
112 errors.extend(mount_errors(
113 &builder,
114 &prefix,
115 shell_assets.is_some(),
116 &served_dirs,
117 ));
118 match &db {
119 Some(db) => {
120 registry.check_all(db, &mounts, &mut errors);
121 if let Err(kind) = crate::auth::check_models_registered(db, &auth) {
122 errors.push(DeclarationError::panel(kind));
123 }
124 }
125 None => errors.push(DeclarationError::panel(DeclarationErrorKind::MissingDb)),
126 }
127 if !errors.is_empty() {
128 return Err(MountError::new(&prefix, errors).into());
129 }
130 if builder.get_app_context::<Panels>().is_none() {
131 builder = install_shared(builder);
132 }
133 let Registry {
134 urls,
135 nav_items,
136 pages,
137 routes,
138 root,
139 children,
140 ..
141 } = registry;
142 let root_redirect = match root {
143 Some(Root::Redirect(target)) => Some(target),
144 Some(Root::Home) | None => None,
145 };
146 let served_paths = served_dirs.iter().map(|(path, _)| path.clone()).collect();
147 let state = Arc::new(PanelState {
148 prefix: prefix.clone(),
149 nav_items,
150 brand,
151 dark_mode: dark_mode.unwrap_or(false),
152 shell_assets,
153 children,
154 mounts,
155 root_redirect: root_redirect.clone(),
156 auth,
157 login_hint,
158 uploads,
159 served_paths,
160 urls,
161 });
162 let prefix_path = route_path(&prefix);
163 builder =
164 builder.layer(topcoat::router::BodyLimit::max(MAX_FORM_BYTES).at(prefix_path.clone()));
165 if let Some(directive) = frame_ancestors {
166 builder = builder.layer(headers::FrameAncestors::new(directive, prefix.clone()));
167 }
168 builder = builder.layer(PanelGate::new(Arc::clone(&state)));
170 if state.gates() {
172 let login_path = route_path(&format!("{prefix}/login"));
173 let logout_path = route_path(&format!("{prefix}/logout"));
174 let tenant_path = route_path(&format!("{prefix}/tenant"));
175 builder = builder
176 .layer(
177 topcoat::router::BodyLimit::max(crate::auth::MAX_LOGIN_BYTES)
178 .at(login_path.clone()),
179 )
180 .route(RouteFn::new(
181 http::Method::GET,
182 login_path.clone(),
183 crate::auth::login_page,
184 ))
185 .route(RouteFn::new(
186 http::Method::POST,
187 login_path,
188 crate::auth::login_post,
189 ))
190 .route(RouteFn::new(
191 http::Method::POST,
192 logout_path,
193 crate::auth::logout_post,
194 ))
195 .route(RouteFn::new(
196 http::Method::POST,
197 tenant_path,
198 crate::auth::tenant_post,
199 ));
200 }
201 builder = builder.layout(LayoutFn::new(
202 prefix_path.clone(),
203 layout.unwrap_or(Panel::layout_shell),
204 ));
205 for (path, dir) in served_dirs {
206 builder = builder
207 .layer(headers::ServedFileHeaders::new(&path))
208 .serve_dir(route_path(&path), dir);
209 }
210 for page in pages {
211 builder = builder.page(page);
212 }
213 for route in routes {
214 builder = builder.route(route);
215 }
216 if root_redirect.is_some() {
217 builder = builder.route(RouteFn::new(
218 http::Method::GET,
219 prefix_path,
220 panel_root_redirect,
221 ));
222 }
223 builder
224 .get_app_context_mut::<Panels>()
225 .expect("the shared panel state is installed above")
226 .0
227 .push(state);
228 Ok(builder)
229 }
230}
231
232impl Registry {
233 fn register_all(
236 &mut self,
237 registrations: Vec<Box<dyn super::register::Registration>>,
238 configuration_errors: Vec<DeclarationError>,
239 ) -> (Arc<Mounts>, Vec<DeclarationError>) {
240 for registration in registrations {
241 registration.register(self);
242 }
243 self.link_relations();
244 let mut errors = configuration_errors;
245 errors.append(&mut self.errors);
246 (Arc::new(std::mem::take(&mut self.mounts)), errors)
247 }
248
249 fn check_all(&self, db: &Db, mounts: &Arc<Mounts>, errors: &mut Vec<DeclarationError>) -> Cx {
251 let cx = validation_cx(db, mounts);
252 for registered in &self.resources {
253 (registered.check)(&cx, errors);
254 }
255 cx
256 }
257}
258
259fn mount_errors(
261 builder: &RouterBuilder,
262 prefix: &str,
263 shell_assets: bool,
264 served_dirs: &[(String, std::path::PathBuf)],
265) -> Vec<DeclarationError> {
266 let mut errors = Vec::new();
267 let mut refuse = |kind| errors.push(DeclarationError::panel(kind));
268 if shell_assets && builder.get_app_context::<AssetConfig>().is_none() {
269 refuse(DeclarationErrorKind::ShellAssetsWithoutBundle);
270 }
271 if under_prefix(RUNTIME_PREFIX, prefix) || under_prefix(prefix, RUNTIME_PREFIX) {
272 refuse(DeclarationErrorKind::PrefixOverlapsRuntime {
273 prefix: prefix.to_string(),
274 });
275 }
276 for (index, (path, _)) in served_dirs.iter().enumerate() {
277 let root = served_root(path);
278 if served_dirs[..index]
279 .iter()
280 .any(|(seen, _)| served_root(seen) == root)
281 {
282 refuse(DeclarationErrorKind::ServeDirTwice { path: path.clone() });
283 }
284 }
285 if let Some(panels) = builder.get_app_context::<Panels>() {
286 for other in &panels.0 {
287 for (path, _) in served_dirs {
288 if other
289 .served_paths
290 .iter()
291 .any(|served| served_root(served) == served_root(path))
292 {
293 refuse(DeclarationErrorKind::ServeDirTaken {
294 path: path.clone(),
295 panel: other.prefix.clone(),
296 });
297 }
298 }
299 if under_prefix(&other.prefix, prefix) || under_prefix(prefix, &other.prefix) {
300 refuse(DeclarationErrorKind::PrefixOverlapsPanel {
301 other: other.prefix.clone(),
302 });
303 }
304 }
305 }
306 errors
307}
308
309fn install_shared(mut builder: RouterBuilder) -> RouterBuilder {
311 builder = builder.cookies();
312 if builder.get_app_context::<SessionConfig>().is_none() {
313 builder = builder.sessions(SessionConfig::builder().lifetime(SESSION_LIFETIME).build());
314 }
315 builder = builder
316 .layer(RuntimeGate::new())
317 .app_context(Panels::default())
318 .app_context(MountScope(|cx| current(cx).map(|panel| &*panel.mounts)))
319 .app_context(TenantSource(crate::auth::session_tenant));
320 if builder.get_app_context::<RuntimeSetup>().is_none() {
323 builder = builder.runtime();
324 }
325 if builder.get_app_context::<PrefetchMode>().is_none() {
326 builder = builder.prefetch(PrefetchMode::Never);
327 }
328 builder
329}
330
331pub(crate) fn panel_root_redirect(cx: &Cx, _body: Body) -> RouteFuture<'_> {
334 Box::pin(async move {
335 crate::auth::guard(cx)?;
337 let target = current(cx)
338 .and_then(|panel| panel.root_redirect.clone())
339 .ok_or_else(topcoat::router::error::not_found)?;
340 Err(redirect(target).into())
341 })
342}
343
344pub(super) fn is_directory_pattern(path: &str) -> bool {
346 Path::from_str(path)
347 .ok()
348 .and_then(|parsed| parsed.segments().next_back())
349 .is_some_and(|segment| segment.as_catch_all().is_some())
350}
351
352pub(super) fn validate_route_segment(
355 item: &'static str,
356 segment: &str,
357) -> Result<(), DeclarationErrorKind> {
358 match segment_fault(segment) {
359 Some(fault) => Err(DeclarationErrorKind::InvalidSegment {
360 item,
361 segment: segment.to_string(),
362 fault,
363 }),
364 None => Ok(()),
365 }
366}
367
368pub(super) type ResourceCheck = fn(&Cx, &mut Vec<DeclarationError>);
371
372pub(super) fn check_resource<R: Resource>(cx: &Cx, errors: &mut Vec<DeclarationError>) {
374 let Ok(declared) = require_mounted::<R>(cx) else {
375 return;
376 };
377 let tenancy = &declared.tenancy;
378 if let Some(Err(kind)) = tenancy.column_field() {
379 errors.push(DeclarationError::of::<R>(Site::Tenancy, kind));
380 }
381 if tenancy.via_is_single() == Some(true) {
382 errors.push(DeclarationError::of::<R>(
383 Site::Tenancy,
384 DeclarationErrorKind::TenancyViaOwnColumn,
385 ));
386 }
387 let form_errors = declared.form.declaration_errors();
388 let form_is_sound = form_errors.is_empty();
389 let view_errors = if declared.has_own_view() {
390 declared.view().declaration_errors()
391 } else {
392 Vec::new()
393 };
394 for (site, kinds) in [
395 (Site::Table, declared.table.declaration_errors()),
396 (Site::Form, form_errors),
397 (Site::View, view_errors),
398 ] {
399 errors.extend(
400 kinds
401 .into_iter()
402 .map(|kind| DeclarationError::of::<R>(site.clone(), kind)),
403 );
404 }
405 check_actions(&declared, errors);
406 check_form_declaration(cx, &declared, form_is_sound, errors);
407}
408
409fn check_actions<R: Resource>(declared: &Mounted<R>, errors: &mut Vec<DeclarationError>) {
413 let mut seen = std::collections::HashSet::new();
414 for action in declared.actions.entries() {
415 if !seen.insert(action.name) {
416 errors.push(DeclarationError::of::<R>(
417 Site::Registration,
418 DeclarationErrorKind::DuplicateAction { name: action.name },
419 ));
420 }
421 }
422}
423
424fn form_error<R: Resource>(kind: DeclarationErrorKind) -> DeclarationError {
426 DeclarationError::of::<R>(Site::Form, kind)
427}
428
429fn check_form_declaration<R: Resource>(
431 cx: &Cx,
432 declared: &Mounted<R>,
433 form_is_sound: bool,
434 errors: &mut Vec<DeclarationError>,
435) {
436 if !form_is_sound {
438 return;
439 }
440 check_layout(declared, errors);
441 if <R::Form as RecordForm>::HAS_FORM {
442 check_form_inner(cx, declared, errors);
443 } else if declared.can(cx, Ability::Create) {
444 errors.push(form_error::<R>(DeclarationErrorKind::CreateWithoutForm));
445 }
446}
447
448fn check_layout<R: Resource>(declared: &Mounted<R>, errors: &mut Vec<DeclarationError>) {
450 let (fields, form) = (declared.fields.as_slice(), &*declared.form);
451 for control in form.fields() {
453 if !fields
454 .iter()
455 .any(|field| field.keys.iter().any(|key| key == control.name()))
456 {
457 errors.push(form_error::<R>(DeclarationErrorKind::UnboundControl {
458 control: control.name().to_string(),
459 }));
460 }
461 }
462 for field in fields {
463 for key in &field.keys {
464 if !form.fields().any(|control| control.name() == key) {
465 errors.push(form_error::<R>(DeclarationErrorKind::MissingControl {
466 field: field.name.to_string(),
467 key: key.clone(),
468 }));
469 }
470 }
471 }
472}
473
474fn check_form_inner<R: Resource>(
475 cx: &Cx,
476 declared: &Mounted<R>,
477 errors: &mut Vec<DeclarationError>,
478) {
479 let (fields, form) = (declared.fields.as_slice(), &*declared.form);
480 if let Some(column) = tenant_column(declared)
482 && let Some(field) = fields.iter().find(|field| field.keys.contains(&column))
483 {
484 errors.push(form_error::<R>(
485 DeclarationErrorKind::FormClaimsTenantColumn {
486 field: field.name.to_string(),
487 column,
488 },
489 ));
490 }
491 for field in form.fields().filter(|field| {
492 field
493 .as_choice()
494 .is_some_and(|choice| choice.has_composite_source())
495 }) {
496 errors.push(form_error::<R>(DeclarationErrorKind::CompositeKeyChoice {
497 field: field.name().to_string(),
498 }));
499 }
500 for field in form.fields() {
501 if let Some(source) = field
502 .as_choice()
503 .and_then(|choice| choice.unavailable_source(cx))
504 {
505 errors.push(form_error::<R>(
506 DeclarationErrorKind::UnregisteredOptionSource {
507 field: field.name().to_string(),
508 source,
509 },
510 ));
511 }
512 }
513 if declared.tenancy.via_is_single() == Some(false) {
514 check_via_foreign_keys(cx, declared, errors);
515 }
516 if declared.can(cx, Ability::Create) {
517 check_create_columns(declared, errors);
518 }
519 let model = R::Model::schema();
520 let root = model.as_root_unwrap();
521 for field in form.fields().filter(|field| field.is_unique()) {
522 let name = field.name();
523 let backed = root
524 .fields
525 .iter()
526 .filter(|field| field.name.app_unwrap() == name)
527 .any(|field| crate::schema::lens_field_unique(field, root));
528 if !backed {
529 errors.push(form_error::<R>(DeclarationErrorKind::UniqueWithoutIndex {
530 field: name.to_string(),
531 }));
532 }
533 if !field.is_required() && !field.is_nullable() {
534 errors.push(form_error::<R>(DeclarationErrorKind::OptionalUnique {
535 field: name.to_string(),
536 }));
537 }
538 }
539}
540
541fn tenant_column<R: Resource>(declared: &Mounted<R>) -> Option<String> {
543 declared
544 .tenancy
545 .column_field()
546 .and_then(Result::ok)
547 .map(|field| field.name.clone())
548}
549
550fn check_create_columns<R: Resource>(declared: &Mounted<R>, errors: &mut Vec<DeclarationError>) {
552 let (fields, create_columns) = (&declared.fields, &declared.create_columns);
553 let prefilled = crate::form::prefilled_fields::<R::Model>();
554 let tenant = tenant_column(declared);
555 if let Some(column) = &tenant
556 && create_columns.contains(&column.as_str())
557 {
558 errors.push(DeclarationError::of::<R>(
559 Site::Registration,
560 DeclarationErrorKind::CreateColumnsNameTenant {
561 column: column.clone(),
562 },
563 ));
564 }
565 let model = R::Model::schema();
566 let root = model.as_root_unwrap();
567 for &column in create_columns {
568 if !root
569 .fields
570 .iter()
571 .any(|field| field.name.app.as_deref() == Some(column))
572 {
573 errors.push(DeclarationError::of::<R>(
574 Site::Registration,
575 DeclarationErrorKind::UnknownCreateColumn { column },
576 ));
577 }
578 }
579 for (index, field) in root.fields.iter().enumerate() {
580 let Some(name) = field.name.app.as_deref() else {
581 continue;
582 };
583 let filled = field.nullable()
584 || field.is_relation()
585 || prefilled.get(index).copied().unwrap_or(false)
586 || tenant.as_deref() == Some(name)
587 || fields.iter().any(|claim| claim.name == name)
588 || create_columns.contains(&name);
589 if !filled {
590 errors.push(form_error::<R>(DeclarationErrorKind::UnwrittenColumn {
591 column: name.to_string(),
592 }));
593 }
594 }
595}
596
597fn validation_cx(db: &Db, mounts: &Arc<Mounts>) -> Cx {
600 let mut app_context = topcoat::context::AppContext::new();
601 app_context.insert(db.clone());
602 app_context.insert(Arc::clone(mounts));
603 app_context.insert(MountScope(|cx| {
604 topcoat::context::try_app_context::<Arc<Mounts>>(cx).map(|mounts| &**mounts)
605 }));
606 Cx::new(Arc::new(app_context))
607}
608
609fn check_via_foreign_keys<R: Resource>(
613 cx: &Cx,
614 declared: &Mounted<R>,
615 errors: &mut Vec<DeclarationError>,
616) {
617 let form = &declared.form;
618 let Some((relation, parent, keys)) = via_relation(declared) else {
619 errors.push(DeclarationError::of::<R>(
620 Site::Tenancy,
621 DeclarationErrorKind::TenancyViaWithoutBelongsTo,
622 ));
623 return;
624 };
625 let unguarded: Vec<String> = keys
626 .into_iter()
627 .filter(|key| {
628 !form.fields().any(|field| {
629 field.name() == key
630 && field
631 .as_choice()
632 .and_then(|choice| choice.tenant_scoped_model(cx))
633 == Some(parent)
634 })
635 })
636 .collect();
637 if !unguarded.is_empty() {
638 errors.push(form_error::<R>(DeclarationErrorKind::UnguardedForeignKey {
639 relation,
640 keys: unguarded,
641 }));
642 }
643}
644
645fn via_relation<R: Resource>(
648 declared: &Mounted<R>,
649) -> Option<(String, toasty::schema::app::ModelId, Vec<String>)> {
650 let hop = declared.tenancy.via_hop()?;
651 let model = R::Model::schema();
652 let root = model.as_root()?;
653 let field = root.fields.get(hop)?;
654 let toasty::schema::app::FieldTy::BelongsTo(relation) = &field.ty else {
655 return None;
656 };
657 let keys = relation
658 .foreign_key
659 .fields
660 .iter()
661 .filter_map(|key| root.fields.get(key.source.index))
662 .map(|field| field.name.app_unwrap().to_string())
663 .collect::<Vec<_>>();
664 (!keys.is_empty()).then(|| (field.name.app_unwrap().to_string(), relation.target, keys))
665}
666
667fn served_root(path: &str) -> &str {
670 path.rsplit_once("{*").map_or(path, |(root, _)| root)
671}
672
673pub(crate) fn route_path(path: &str) -> topcoat::router::PathBuf {
675 Path::from_str(path)
676 .expect("panel route paths are well-formed")
677 .to_owned()
678}
679
680#[cfg(test)]
681mod tests;