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