Skip to main content

renox_core/
app.rs

1use std::collections::HashMap;
2use std::future::Future;
3use std::net::SocketAddr;
4use std::sync::Arc;
5use std::time::Duration;
6
7use anyhow::anyhow;
8use axum::Router;
9use axum::handler::HandlerWithoutStateExt;
10use axum::middleware::{from_fn, from_fn_with_state};
11use tokio::net::TcpListener;
12use tokio::sync::watch;
13use tower_http::services::ServeDir;
14use tower_http::trace::TraceLayer;
15use tracing_subscriber::EnvFilter;
16
17use crate::auth::{Gate, LoginThrottle, User};
18use crate::crypto::parse_key;
19use crate::db::{Db, Migration, MigrationStatus, Migrator};
20use crate::events::Event;
21use crate::mail::Mailer;
22use crate::queue::{Handlers, Job, Queue, Worker};
23use crate::routing::RouteInfo;
24use crate::schedule::Schedule;
25use crate::timezone::Zone;
26use crate::{
27    AppState, Config, Environment, Error, Module, Registry, Result, RouteTable, Views, assets,
28    auth, csrf, session, view,
29};
30
31/// How long `serve` waits for running jobs after a shutdown signal.
32const SHUTDOWN_GRACE: Duration = Duration::from_secs(30);
33
34const USAGE: &str = "\
35Usage: <app> [command]
36
37Commands:
38  serve                     Start the web server, queue workers and scheduler (default)
39  migrate                   Run pending migrations
40  migrate:rollback [--step N]
41                            Undo the last N batches of migrations (default 1)
42  migrate:fresh [--seed]    Drop all tables, run every migration, optionally seed
43  migrate:status            List migrations and whether they have run
44  db:check                  Compare the registered models with the tables the migrations build
45  db:diff [name] [--yes]    Write a migration for what changed in the registered models
46  db:seed                   Run the seeders
47  queue:work [--queue a,b] [--workers N] [--once]
48                            Run queued jobs (until stopped, or --once for what's there)
49  queue:failed              List failed jobs
50  queue:retry <id|all>      Put failed jobs back on the queue
51  queue:flush               Delete failed jobs
52  queue:forget <id>         Delete one failed job
53  queue:prune-failed [--hours N]
54                            Delete failed jobs older than N hours (default 168)
55  queue:prune-batches [--hours N]
56                            Delete batches finished more than N hours ago (default 24)
57  webhook:failed            List webhook calls whose processing failed
58  webhook:retry <id>        Process a stored webhook call again
59  ui:publish [--force]      Copy the UI kit (renox/ui.html and its CSS) into the app to change it
60  cache:prune               Delete expired rows of the database cache store
61  session:prune             Delete expired sessions (SESSION_DRIVER=database)
62  schedule:list             List scheduled tasks and when they run next
63  schedule:run <task>       Run one scheduled task now
64  schedule:work             Run scheduled tasks (when SCHEDULER=false for serve)
65  route:list                List every route with its name, module and guards
66  view:check                Compile every template; check route names and typed views
67  view:data [--out P]       Write .vscode/renox-components.json for editor autocomplete
68  db:shell                  Run SQL against the database (`.tables`, `.quit`)
69  down [--secret S] [--retry N]
70                            Maintenance mode: answer 503 (visit /S to bypass it)
71  up                        Leave maintenance mode
72  help                      Show this message";
73
74/// The application builder.
75///
76/// ```no_run
77/// # use renox::prelude::*;
78/// # use serde::{Deserialize, Serialize};
79/// # struct Products;
80/// # impl Module for Products { fn name(&self) -> &'static str { "products" } }
81/// # #[derive(Serialize, Deserialize)] struct SendReceipt;
82/// # impl Job for SendReceipt { const NAME: &'static str = "send-receipt"; async fn handle(self, _: JobContext) -> Result { Ok(()) } }
83/// # async fn cleanup(_: AppState) -> Result { Ok(()) }
84/// # async fn seed(_: AppState) -> Result { Ok(()) }
85/// fn main() -> renox::Result {
86///     App::new()
87///         .migrations(renox::migrations!())
88///         .module(Products)
89///         .job::<SendReceipt>()
90///         .schedule(|s| { s.daily_at("02:00", "cleanup", cleanup); })
91///         .seeder(seed)
92///         .run()
93/// }
94/// ```
95///
96/// The built binary is also the app's command line, like Laravel's artisan:
97/// `my-app migrate`, `my-app queue:work`, `my-app help`.
98pub struct App {
99    config: Option<Config>,
100    modules: Vec<Box<dyn Module>>,
101    migrations: Vec<Migration>,
102    gates: HashMap<String, Gate>,
103    async_gates: HashMap<String, crate::auth::AsyncGate>,
104    gate_before: Option<crate::auth::GateBefore>,
105    registry: Registry,
106    embedded: Option<crate::Embedded>,
107    csp: crate::security::Csp,
108    permissions_policy: crate::security::PermissionsPolicy,
109    provided: HashMap<std::any::TypeId, Arc<dyn std::any::Any + Send + Sync>>,
110    layers: Vec<AppLayer>,
111    limiters: HashMap<String, crate::rate_limit::LimitRule>,
112    detect_locale: bool,
113    disks: Vec<(String, DiskSettings)>,
114    mailers: Vec<(String, MailerSettings)>,
115    xsrf_cookie: bool,
116}
117
118/// How `App::mailer` gets a mailer's settings once the configuration is loaded.
119type MailerSettings = Box<dyn Fn(&Config) -> Result<crate::mail::MailConfig> + Send + Sync>;
120
121/// How `App::disk` gets a disk's settings once the configuration is loaded.
122type DiskSettings = Box<dyn Fn(&Config) -> Result<crate::storage::StorageConfig> + Send + Sync>;
123
124/// A layer from `App::layer`, applied to the app's routes at boot.
125/// Applied to the app's router and to each `Routes::domain` router.
126type AppLayer = Box<dyn Fn(Router<AppState>) -> Router<AppState> + Send + Sync>;
127
128impl App {
129    /// Creates an application that loads its configuration from `.env` on start.
130    pub fn new() -> Self {
131        Self {
132            config: None,
133            modules: Vec::new(),
134            migrations: Vec::new(),
135            gates: HashMap::new(),
136            async_gates: HashMap::new(),
137            gate_before: None,
138            registry: Registry::default(),
139            embedded: None,
140            csp: crate::security::Csp::default(),
141            permissions_policy: crate::security::PermissionsPolicy::default(),
142            provided: HashMap::new(),
143            layers: Vec::new(),
144            limiters: HashMap::new(),
145            detect_locale: false,
146            disks: Vec::new(),
147            mailers: Vec::new(),
148            xsrf_cookie: false,
149        }
150    }
151
152    /// Picks a visitor's language from their browser (`Accept-Language`)
153    /// when they haven't chosen one: the first of their languages this app
154    /// has texts for (the built-in `en` and `id`, or a
155    /// `resources/lang/<locale>.json`), else `APP_LOCALE`. A language set
156    /// with `i18n::remember_locale` still wins. Responses then carry
157    /// `Vary: Accept-Language`, so caches keep the languages apart.
158    ///
159    /// ```
160    /// # use renox::prelude::*;
161    /// # let _ =
162    /// App::new().detect_locale()
163    /// # ;
164    /// ```
165    pub fn detect_locale(mut self) -> Self {
166        self.detect_locale = true;
167        self
168    }
169
170    /// Wraps every route of the app's modules in a tower layer, e.g. a
171    /// middleware function (framework routes such as `/health` and
172    /// `public/` files aren't wrapped). It runs after Renox has loaded the
173    /// session and the user, so it can use `AuthUser` or `Session`:
174    ///
175    /// ```
176    /// # use renox::prelude::*;
177    /// use renox::axum::extract::Request;
178    /// use renox::axum::middleware::{Next, from_fn};
179    ///
180    /// async fn stamp(user: Option<AuthUser>, req: Request, next: Next) -> Response {
181    ///     let mut res = next.run(req).await;
182    ///     let who = if user.is_some() { "member" } else { "guest" };
183    ///     res.headers_mut().insert("x-visitor", who.parse().unwrap());
184    ///     res
185    /// }
186    ///
187    /// # let _ =
188    /// App::new().layer(from_fn(stamp))
189    /// # ;
190    /// ```
191    ///
192    /// Layers run in the order added: the first one sees the request first.
193    pub fn layer<L>(mut self, layer: L) -> Self
194    where
195        L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
196        L::Service: tower::Service<axum::extract::Request> + Clone + Send + Sync + 'static,
197        <L::Service as tower::Service<axum::extract::Request>>::Response:
198            axum::response::IntoResponse + 'static,
199        <L::Service as tower::Service<axum::extract::Request>>::Error:
200            Into<std::convert::Infallible> + 'static,
201        <L::Service as tower::Service<axum::extract::Request>>::Future: Send + 'static,
202    {
203        self.layers
204            .push(Box::new(move |router| router.layer(layer.clone())));
205        self
206    }
207
208    /// Allows other sites in the Content-Security-Policy, e.g.
209    /// `.csp(|csp| { csp.allow("script-src", "https://www.googletagmanager.com"); })`.
210    pub fn csp(mut self, allow: impl FnOnce(&mut crate::security::Csp)) -> Self {
211        allow(&mut self.csp);
212        self
213    }
214
215    /// Allows browser features in the `Permissions-Policy`, which switches off
216    /// the ones a typical app never uses (camera, microphone, geolocation,
217    /// payment, USB…): `.permissions_policy(|p| { p.allow("geolocation", "self"); })`.
218    /// A feature can be allowed for `self`, for an origin
219    /// (`"https://pay.example.com"`) or for everyone (`"*"`). `PERMISSIONS_POLICY`
220    /// in `.env` sets the base: `default`, `off` or a full header value.
221    ///
222    /// ```
223    /// # use renox::prelude::*;
224    /// # let _ =
225    /// App::new().permissions_policy(|p| {
226    ///     p.allow("camera", "self");
227    /// })
228    /// # ;
229    /// ```
230    pub fn permissions_policy(
231        mut self,
232        allow: impl FnOnce(&mut crate::security::PermissionsPolicy),
233    ) -> Self {
234        allow(&mut self.permissions_policy);
235        self
236    }
237
238    /// Views, translations and public files compiled into the binary:
239    /// `.embed(renox::embedded!())`. They're used when `APP_DEBUG` is off;
240    /// while debugging, files are read from disk so edits show up at once.
241    pub fn embed(mut self, embedded: crate::Embedded) -> Self {
242        self.embedded = Some(embedded);
243        self
244    }
245
246    /// Uses the given configuration instead of loading it from the environment.
247    pub fn with_config(config: Config) -> Self {
248        Self {
249            config: Some(config),
250            ..Self::new()
251        }
252    }
253
254    /// Replaces the configuration, e.g. in tests.
255    pub fn config(mut self, config: Config) -> Self {
256        self.config = Some(config);
257        self
258    }
259
260    /// Adds a module: its routes, migrations and registrations.
261    pub fn module(mut self, module: impl Module) -> Self {
262        self.modules.push(Box::new(module));
263        self
264    }
265
266    /// Registers app-level migrations, usually `renox::migrations!()`.
267    pub fn migrations(mut self, migrations: &[Migration]) -> Self {
268        self.migrations.extend_from_slice(migrations);
269        self
270    }
271
272    /// Registers a seeder for `db:seed`. Seeders run in registration order
273    /// (the app's, then each module's from `Registry::seeder`, in the order
274    /// the modules were added), in the app's context (model hooks and `renox::context::app()` see
275    /// it), and get the app's state: `state.db`, its config, storage…
276    ///
277    /// ```
278    /// # use renox::prelude::*;
279    /// # #[derive(Model, serde::Serialize, Default)] struct Product { id: i64 }
280    /// # impl Factory for Product { fn definition() -> Self { Product::default() } }
281    /// # let _ =
282    /// App::new().seeder(|state| async move {
283    ///     Product::factory().count(50).create(&state.db).await?;
284    ///     Ok(())
285    /// })
286    /// # ;
287    /// ```
288    pub fn seeder<F, Fut>(mut self, seeder: F) -> Self
289    where
290        F: Fn(AppState) -> Fut + Send + Sync + 'static,
291        Fut: Future<Output = Result> + Send + 'static,
292    {
293        self.registry.seeder(seeder);
294        self
295    }
296
297    /// Defines a gate: an ability that depends only on the user.
298    ///
299    /// ```
300    /// # use renox::prelude::*;
301    /// # let _ =
302    /// App::new().gate("admin", |user| user.email.ends_with("@shop.example"))
303    /// // in a handler: auth.gate("admin")?;   in a template: {% if can('admin') %}
304    /// # ;
305    /// ```
306    pub fn gate(
307        mut self,
308        name: &str,
309        check: impl Fn(&User) -> bool + Send + Sync + 'static,
310    ) -> Self {
311        self.gates.insert(name.to_owned(), Arc::new(check));
312        self
313    }
314
315    /// A gate that may query the database, e.g. whether the user belongs to
316    /// a team. Check it in handlers with `auth.gate_async(name).await?`;
317    /// templates can't wait for it (`can()` denies it), so pass its answer in
318    /// the view's context.
319    ///
320    /// ```
321    /// # use renox::prelude::*;
322    /// # let _ =
323    /// App::new().gate_async("billing", |user, state| async move {
324    ///     let n: i64 = renox::db::sql("SELECT COUNT(*) FROM team_admins WHERE user_id = ?")
325    ///         .bind(user.id)
326    ///         .scalar(&state.db)
327    ///         .await?;
328    ///     Ok(n > 0)
329    /// })
330    /// // in a handler: auth.gate_async("billing").await?;
331    /// # ;
332    /// ```
333    /// Asked before every gate, permission and policy check: `Some(true)`
334    /// allows, `Some(false)` denies, `None` goes on to the check itself.
335    /// Typically lets super-admins do everything.
336    ///
337    /// ```
338    /// # use renox::prelude::*;
339    /// # let _ =
340    /// App::new().gate_before(|user, _ability| {
341    ///     (user.get::<String>("role").as_deref() == Some("owner")).then_some(true)
342    /// })
343    /// # ;
344    /// ```
345    pub fn gate_before(
346        mut self,
347        check: impl Fn(&User, &str) -> Option<bool> + Send + Sync + 'static,
348    ) -> Self {
349        self.gate_before = Some(Arc::new(check));
350        self
351    }
352
353    /// Defines the gate `name` with a check that can await, e.g. to query
354    /// the database. `can(name)` and `require_gate` ask it like any gate.
355    pub fn gate_async<F, Fut>(mut self, name: &str, check: F) -> Self
356    where
357        F: Fn(User, AppState) -> Fut + Send + Sync + 'static,
358        Fut: Future<Output = Result<bool>> + Send + 'static,
359    {
360        self.async_gates.insert(
361            name.to_owned(),
362            Arc::new(move |user, state| Box::pin(check(user, state))),
363        );
364        self
365    }
366
367    /// Receives `W`'s webhooks: see `renox::webhook`. Also add the route
368    /// with `Routes::webhook::<W>(path)`.
369    pub fn webhook<W: crate::webhook::Webhook>(mut self) -> Self {
370        self.registry.webhook::<W>();
371        self
372    }
373
374    /// Registers a model for `db:check`, which compares its fields with the
375    /// table. Registering it twice keeps one.
376    ///
377    /// ```
378    /// # use renox::prelude::*;
379    /// # #[derive(Model, serde::Serialize, Default)] struct Product { id: i64 }
380    /// let app = App::new().model::<Product>();
381    /// ```
382    pub fn model<M: crate::db::Model>(mut self) -> Self {
383        self.registry.model::<M>();
384        self
385    }
386
387    /// Registers a page struct for `view:check`, which then reports the variables its
388    /// template uses that the struct lacks; see [`crate::ViewData`]. Registering it twice
389    /// keeps one.
390    ///
391    /// ```
392    /// # use renox::prelude::*;
393    /// #[derive(serde::Serialize, ViewData)]
394    /// #[view(template = "suppliers/index.html")]
395    /// struct SupplierList { suppliers: Vec<String> }
396    ///
397    /// let app = App::new().view_data::<SupplierList>();
398    /// ```
399    pub fn view_data<T: crate::ViewData>(mut self) -> Self {
400        self.registry.view_data::<T>();
401        self
402    }
403
404    /// Registers the live component `C` (see `renox::live_component`).
405    pub fn live_component<C: crate::live_component::LiveComponent>(mut self) -> Self {
406        self.registry.live_component::<C>();
407        self
408    }
409
410    /// Lets queue workers run jobs of type `J`.
411    pub fn job<J: Job>(mut self) -> Self {
412        self.registry.job::<J>();
413        self
414    }
415
416    /// Runs `listener` whenever an `E` is emitted with `state.emit(..)`.
417    pub fn listen<E, F, Fut>(mut self, listener: F) -> Self
418    where
419        E: Event,
420        F: Fn(E, AppState) -> Fut + Send + Sync + 'static,
421        Fut: Future<Output = Result> + Send + 'static,
422    {
423        self.registry.listen(listener);
424        self
425    }
426
427    /// Adds a command the app binary runs: `my-app <name> [args]`, e.g. to
428    /// create the first admin or run an import. See [`crate::command`].
429    pub fn command<F, Fut>(mut self, name: &str, about: &str, run: F) -> Self
430    where
431        F: Fn(crate::command::Args, AppState) -> Fut + Send + Sync + 'static,
432        Fut: Future<Output = Result> + Send + 'static,
433    {
434        self.registry.command(name, about, run);
435        self
436    }
437
438    /// A command whose arguments are declared with clap; see
439    /// [`AppCommand`](crate::command::AppCommand).
440    pub fn typed_command<T: crate::command::AppCommand>(mut self) -> Self {
441        self.registry.typed_command::<T>();
442        self
443    }
444
445    /// A named rate limit for `Routes::throttle_by(name)`, whose `rule`
446    /// picks the limit for each request (by user, role, IP, API key…); see
447    /// [`crate::rate_limit::Limit`].
448    pub fn rate_limiter(
449        mut self,
450        name: &str,
451        rule: impl Fn(&crate::rate_limit::LimitRequest) -> crate::rate_limit::Limit
452        + Send
453        + Sync
454        + 'static,
455    ) -> Self {
456        self.limiters
457            .insert(name.to_owned(), std::sync::Arc::new(rule));
458        self
459    }
460
461    /// Sends every error that needs a person (a 500, a job that failed for
462    /// good, a failed scheduled task) to `reporter`, e.g. an error tracker;
463    /// see [`crate::report`].
464    pub fn report<F, Fut>(mut self, reporter: F) -> Self
465    where
466        F: Fn(crate::report::ErrorReport, AppState) -> Fut + Send + Sync + 'static,
467        Fut: Future<Output = ()> + Send + 'static,
468    {
469        self.registry.report(reporter);
470        self
471    }
472
473    /// Adds a notification channel (WhatsApp, SMS, Slack…); see
474    /// [`Registry::channel`].
475    pub fn channel<F, Fut>(mut self, name: &str, send: F) -> Self
476    where
477        F: Fn(crate::auth::Recipient, serde_json::Value, AppState) -> Fut + Send + Sync + 'static,
478        Fut: Future<Output = Result> + Send + 'static,
479    {
480        self.registry.channel(name, send);
481        self
482    }
483
484    /// Also sends the CSRF token as an `XSRF-TOKEN` cookie that scripts can
485    /// read, and accepts it back in an `X-XSRF-TOKEN` header (Laravel's
486    /// behaviour), for a JavaScript client on the same site: axios sends
487    /// it by itself. Forms and htmx don't need it; they send `_token` or
488    /// `X-CSRF-Token`.
489    pub fn xsrf_cookie(mut self) -> Self {
490        self.xsrf_cookie = true;
491        self
492    }
493
494    /// Adds a mailer named `name`, e.g. a provider for newsletters or a
495    /// second SMTP account, used with `state.mailer_named(name)` or, by
496    /// listing it in `MAIL_FAILOVER`, when the default mailer fails.
497    /// `settings` runs once the configuration is loaded;
498    /// [`MailConfig::from_env`] reads `<PREFIX>_MAILER`, `_HOST`, …
499    ///
500    /// ```
501    /// # use renox::prelude::*;
502    /// use renox::mail::MailConfig;
503    ///
504    /// # let _ =
505    /// // BACKUP_HOST=smtp.other-provider.com, BACKUP_USERNAME=…; MAIL_FAILOVER=backup
506    /// App::new().mailer("backup", |config| MailConfig::from_env(config, "BACKUP"))
507    /// # ;
508    /// ```
509    ///
510    /// [`MailConfig::from_env`]: crate::mail::MailConfig::from_env
511    pub fn mailer(
512        mut self,
513        name: &str,
514        settings: impl Fn(&Config) -> Result<crate::mail::MailConfig> + Send + Sync + 'static,
515    ) -> Self {
516        self.mailers.push((name.to_owned(), Box::new(settings)));
517        self
518    }
519
520    /// Adds a storage disk named `name` (letters, digits, `-`, `_`), e.g.
521    /// backups on another bucket, read with `state.disk_named(name)`. `settings`
522    /// runs once the configuration is loaded; [`StorageConfig::from_env`]
523    /// reads `<PREFIX>_DISK`, `<PREFIX>_BUCKET`, … A local disk keeps its
524    /// files in `STORAGE_PATH/<name>` unless its settings say otherwise.
525    ///
526    /// ```
527    /// # use renox::prelude::*;
528    /// use renox::storage::StorageConfig;
529    ///
530    /// # let _ =
531    /// App::new()
532    ///     .disk("backups", |config| StorageConfig::from_env(config, "BACKUPS"))
533    ///     .disk("exports", |_| Ok(StorageConfig::default())) // local: storage/exports
534    /// # ;
535    /// ```
536    ///
537    /// [`StorageConfig::from_env`]: crate::storage::StorageConfig::from_env
538    pub fn disk(
539        mut self,
540        name: &str,
541        settings: impl Fn(&Config) -> Result<crate::storage::StorageConfig> + Send + Sync + 'static,
542    ) -> Self {
543        self.disks.push((name.to_owned(), Box::new(settings)));
544        self
545    }
546
547    /// Makes `value` available everywhere the app runs: `Provided<T>` in
548    /// handlers, `state.provided::<T>()` in jobs, listeners, commands and
549    /// scheduled tasks. One value per type; a second one replaces the first,
550    /// and it wins over a module's ([`Registry::provide`]). See
551    /// [`crate::Provided`].
552    pub fn provide<T: Send + Sync + 'static>(mut self, value: T) -> Self {
553        self.provided
554            .insert(std::any::TypeId::of::<T>(), Arc::new(value));
555        self
556    }
557
558    /// Gives every view a value computed per request; see [`Registry::share`].
559    pub fn share<F, Fut, T>(mut self, key: &str, compute: F) -> Self
560    where
561        F: Fn(crate::view::ViewContext) -> Fut + Send + Sync + 'static,
562        Fut: Future<Output = Result<T>> + Send + 'static,
563        T: serde::Serialize,
564    {
565        self.registry.share(key, compute);
566        self
567    }
568
569    /// Adds template functions, filters or globals; see [`Registry::templates`].
570    pub fn templates(
571        mut self,
572        hook: impl Fn(&mut minijinja::Environment<'static>) + Send + Sync + 'static,
573    ) -> Self {
574        self.registry.templates(hook);
575        self
576    }
577
578    /// Defines scheduled tasks.
579    pub fn schedule(mut self, define: impl FnOnce(&mut Schedule)) -> Self {
580        define(self.registry.schedule());
581        self
582    }
583
584    /// Connects to the database and builds the router, without serving.
585    pub async fn boot(mut self) -> Result<Kernel> {
586        let config = match self.config {
587            Some(config) => config,
588            None => Config::load()?,
589        };
590
591        self.registry.job::<crate::mail::SendMail>();
592        self.registry.job::<crate::mail::SendMailVia>();
593        self.registry
594            .job::<crate::auth::notifications::SendToChannel>();
595        self.registry.job::<crate::webhook::ProcessWebhook>();
596        #[cfg(feature = "server-events")]
597        self.registry.job::<crate::analytics::ServerEvent>();
598        let mut migrations = crate::queue::MIGRATIONS.to_vec();
599        migrations.push(crate::cache::MIGRATION);
600        migrations.push(crate::session::MIGRATION);
601        migrations.push(crate::grid::MIGRATION);
602        migrations.extend(crate::webhook::MIGRATIONS);
603        migrations.extend(self.migrations);
604        for module in &self.modules {
605            migrations.extend_from_slice(module.migrations());
606            module.register(&mut self.registry);
607        }
608        let Registry {
609            jobs,
610            listeners,
611            schedule,
612            duplicate_job,
613            webhooks,
614            live_components,
615            duplicate_live,
616            commands,
617            templates,
618            components,
619            shares,
620            channels,
621            reporters,
622            permissions,
623            second_factor,
624            duplicate_second_factor,
625            mut account_sections,
626            auth,
627            assets: static_assets,
628            provided: mut module_provided,
629            seeders,
630            models,
631            view_data,
632        } = self.registry;
633        // The app's own `App::provide` values win over the modules'.
634        module_provided.extend(self.provided);
635        check_assets(&static_assets)?;
636        let static_assets: Arc<[crate::registry::StaticAsset]> = static_assets.into();
637        if let Some(name) = duplicate_job {
638            return Err(anyhow!("job `{name}` is registered twice").into());
639        }
640        if let Some(name) = duplicate_live {
641            return Err(anyhow!("live component `{name}` is registered twice").into());
642        }
643        if duplicate_second_factor {
644            return Err(
645                anyhow!("two modules set a second login step (`Registry::second_factor`)").into(),
646            );
647        }
648        check_commands(&commands)?;
649        schedule.check()?;
650        let zone: Zone = config.timezone;
651
652        let migrator = Migrator::new(migrations)?;
653        let key = match &config.key {
654            Some(key) => parse_key(key)?,
655            None => parse_key(&crate::generate_key())?,
656        };
657        // `Encrypted` model fields are sealed and opened with APP_KEY.
658        let db = crate::db::connect(&config).await?.with_key(key.clone());
659
660        let mut router = Router::new();
661        let mut fallback: Option<axum::routing::MethodRouter<AppState>> = None;
662        // One router per `Routes::domain` pattern, with its own fallback.
663        let mut domains: Vec<(
664            crate::domain::DomainPattern,
665            Router<AppState>,
666            Option<axum::routing::MethodRouter<AppState>>,
667        )> = Vec::new();
668        let mut routes = RouteTable::default();
669        let mut listing: Vec<RouteInfo> = Vec::new();
670        for module in &self.modules {
671            tracing::debug!(module = module.name(), "registering module");
672            let parts = module.routes().into_parts();
673            check_clashes(&listing, &parts.listing, None, module.name())?;
674            router = merge_routes(router, parts.router, module.name())?;
675            for (name, path) in parts.names {
676                routes.insert(name, path)?;
677            }
678            listing.extend(parts.listing.into_iter().map(|info| RouteInfo {
679                module: module.name().to_owned(),
680                ..info
681            }));
682            if let Some(handler) = parts.fallback {
683                if fallback.is_some() {
684                    return Err(anyhow!(
685                        "two modules set a fallback route (`{}` is the second)",
686                        module.name()
687                    )
688                    .into());
689                }
690                fallback = Some(handler);
691            }
692            for (text, domain_routes) in parts.domains {
693                let pattern = crate::domain::DomainPattern::parse(&text)?;
694                let inner = domain_routes.into_parts();
695                if !inner.domains.is_empty() {
696                    return Err(anyhow!(
697                        "Routes::domain(\"{text}\", …) inside another domain (module `{}`)",
698                        module.name()
699                    )
700                    .into());
701                }
702                check_clashes(
703                    &listing,
704                    &inner.listing,
705                    Some(pattern.as_str()),
706                    module.name(),
707                )?;
708                let at = match domains.iter().position(|(p, _, _)| *p == pattern) {
709                    Some(at) => at,
710                    None => {
711                        domains.push((pattern.clone(), Router::new(), None));
712                        domains.len() - 1
713                    }
714                };
715                let (_, domain_router, domain_fallback) = &mut domains[at];
716                *domain_router =
717                    merge_routes(std::mem::take(domain_router), inner.router, module.name())?;
718                if let Some(handler) = inner.fallback {
719                    if domain_fallback.is_some() {
720                        return Err(anyhow!(
721                            "two fallbacks for the domain `{text}` (module `{}`)",
722                            module.name()
723                        )
724                        .into());
725                    }
726                    *domain_fallback = Some(handler);
727                }
728                for (name, path) in inner.names {
729                    routes.set_domain(&name, pattern.as_str());
730                    routes.insert(name, path)?;
731                }
732                listing.extend(inner.listing.into_iter().map(|info| RouteInfo {
733                    module: module.name().to_owned(),
734                    domain: Some(pattern.as_str().to_owned()),
735                    ..info
736                }));
737            }
738        }
739        // The app's own layers; the first one added ends up outermost.
740        for layer in self.layers.iter().rev() {
741            router = layer(router);
742            for (_, domain_router, _) in &mut domains {
743                *domain_router = layer(std::mem::take(domain_router));
744            }
745        }
746        listing.extend(framework_routes(&config));
747        listing.sort_by(|a, b| (&a.path, &a.method).cmp(&(&b.path, &b.method)));
748        // The current route's name follows the request's method (#299).
749        for info in &listing {
750            if let Some(name) = &info.name {
751                routes.set_method(name, &info.method);
752            }
753        }
754        // `throttle_by("name")` needs `App::rate_limiter("name", …)`.
755        for route in &listing {
756            for mark in &route.middleware {
757                if let Some(name) = mark.strip_prefix("throttle:")
758                    && !name.contains('/')
759                    && !self.limiters.contains_key(name)
760                {
761                    return Err(anyhow!(
762                        "{} {} uses throttle_by(\"{name}\"), but there's no App::rate_limiter(\"{name}\", …)",
763                        route.method,
764                        route.path
765                    )
766                    .into());
767                }
768            }
769        }
770        for route in &listing {
771            for provider in route
772                .middleware
773                .iter()
774                .filter_map(|m| m.strip_prefix("webhook:"))
775            {
776                if !webhooks.contains_key(provider) {
777                    return Err(anyhow!(
778                        "the route {} receives `{provider}` webhooks, but they're not registered: \
779                         add `app.webhook::<…>()` in the module's `register`",
780                        route.path
781                    )
782                    .into());
783                }
784            }
785        }
786        let routes = Arc::new(routes);
787
788        let storage = crate::storage::Storage::from_config(&config)?;
789        let mut disks = HashMap::new();
790        for (name, settings) in &self.disks {
791            let valid = !name.is_empty()
792                && name
793                    .chars()
794                    .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
795            if !valid {
796                return Err(anyhow!(
797                    "disk name `{name}`: use letters, digits, `-` and `_` (App::disk)"
798                )
799                .into());
800            }
801            let disk = crate::storage::Storage::named(&config, name, &settings(&config)?)?;
802            if disks.insert(name.clone(), disk).is_some() {
803                return Err(anyhow!("two disks are named `{name}` (App::disk)").into());
804            }
805        }
806        // Release builds serve what was compiled in; debug builds read the disk.
807        let embedded = self.embedded.filter(|_| !config.debug);
808        // Rate limits and login locks shared by every server (CACHE_STORE=database).
809        let shared_counters =
810            (config.cache_store == crate::CacheStore::Database).then(|| db.clone());
811        let versions = Arc::new(crate::embedded::AssetVersions::new(
812            &config.public_path,
813            embedded.map(|e| e.public),
814        ));
815        let views = Views::new(
816            &config,
817            routes.clone(),
818            storage.clone(),
819            embedded.map(|e| e.views),
820            Arc::new(templates),
821            Arc::new(components.iter().map(|c| c.contract()).collect()),
822            zone,
823            versions,
824        )
825        .with_typed(
826            view_data,
827            shares.iter().map(|(key, _)| key.clone()).collect(),
828        );
829        let mut security =
830            crate::security::Security::new(&config, &self.csp, &self.permissions_policy, &listing)?;
831        security.xsrf_cookie = self.xsrf_cookie;
832        let security = Arc::new(security);
833        let mut mailers = HashMap::new();
834        for (name, settings) in &self.mailers {
835            let mailer = Mailer::open(&settings(&config)?, &config)?;
836            if mailers.insert(name.clone(), mailer).is_some() {
837                return Err(anyhow!("two mailers are named `{name}` (App::mailer)").into());
838            }
839        }
840        let mut failover = Vec::new();
841        for name in &config.mail.failover {
842            let mailer = mailers.get(name).ok_or_else(|| {
843                anyhow!("MAIL_FAILOVER names `{name}`, but no mailer has that name: add it with App::mailer")
844            })?;
845            failover.push((name.clone(), mailer.clone()));
846        }
847        let mailer = Mailer::from_config(&config)?.with_failover(failover);
848        let state = AppState::new(crate::state::AppStateInner {
849            security,
850            webhooks: Arc::new(webhooks),
851            live_components: Arc::new(live_components),
852            mailer,
853            mailers: Arc::new(mailers),
854            queue: Queue::new(db.clone(), key.clone()),
855            cache: crate::cache::Cache::new(config.cache_store, db.clone())?,
856            storage,
857            disks: Arc::new(disks),
858            http: crate::http::Http::default(),
859            fakes: Arc::default(),
860            translator: Arc::new(match embedded {
861                Some(files) => crate::i18n::Translator::embedded(files.lang)?,
862                None => crate::i18n::Translator::load(&config.lang_path, config.debug)?,
863            }),
864            // Tests read database sessions synchronously (`TestApp::session_get`).
865            session_mirror: (config.session_driver == crate::SessionDriver::Database
866                && config.env == Environment::Testing)
867                .then(Default::default),
868            live: (config.debug && config.env == Environment::Local).then(|| {
869                crate::live::Live::start(vec![
870                    config.views_path.clone(),
871                    config.public_path.clone(),
872                    config.lang_path.clone(),
873                ])
874            }),
875            notification_hub: Arc::new(crate::auth::notifications::Hub::new()),
876            listeners: Arc::new(listeners),
877            inspector: (config.debug && config.env == Environment::Local)
878                .then(|| Arc::new(crate::inspector::Inspector::default())),
879            config: Arc::new(config),
880            routes,
881            views,
882            db,
883            key,
884            gates: Arc::new(crate::auth::Access {
885                gates: self.gates,
886                before: self.gate_before,
887                permissions,
888            }),
889            async_gates: Arc::new(self.async_gates),
890            shares: Arc::new(shares),
891            channels: Arc::new(channels),
892            reporters: Arc::new(reporters),
893            second_factor: second_factor.map(Arc::new),
894            account_sections: {
895                account_sections.sort_by_key(|section| section.order);
896                Arc::new(account_sections)
897            },
898            auth,
899            limiters: Arc::new(
900                self.limiters
901                    .into_iter()
902                    .map(|(name, rule)| (name, crate::rate_limit::NamedLimiter::new(rule)))
903                    .collect(),
904            ),
905            provided: Arc::new(module_provided),
906            throttle: Arc::new(LoginThrottle::new(shared_counters.clone())),
907            detect_locale: self.detect_locale,
908        });
909        if let Some(second) = &state.second_factor
910            && state.url(&second.challenge, &[]).is_err()
911        {
912            return Err(anyhow!(
913                "the second login step's challenge route `{}` doesn't exist",
914                second.challenge
915            )
916            .into());
917        }
918
919        let public = embedded.map(|e| e.public);
920        let default = build_router(router, state.clone(), public, fallback, &static_assets);
921        let router = if domains.is_empty() {
922            default
923        } else {
924            let hosts = domains
925                .into_iter()
926                .map(|(pattern, router, fallback)| {
927                    (
928                        pattern,
929                        build_router(router, state.clone(), public, fallback, &static_assets),
930                    )
931                })
932                .collect();
933            crate::domain::dispatch(hosts, default)
934        };
935        Ok(Kernel {
936            listing,
937            router,
938            state,
939            migrator,
940            seeders,
941            models,
942            handlers: Arc::new(jobs),
943            schedule,
944            zone,
945            commands,
946        })
947    }
948
949    /// Builds the router without starting a server, e.g. for tests.
950    pub async fn into_router(self) -> Result<Router> {
951        Ok(self.boot().await?.router)
952    }
953
954    /// Runs the command given on the command line (`serve` by default) on a
955    /// new Tokio runtime.
956    pub fn run(self) -> Result {
957        let args: Vec<String> = std::env::args().skip(1).collect();
958        tokio::runtime::Builder::new_multi_thread()
959            .enable_all()
960            .build()?
961            .block_on(self.run_command(&args))
962    }
963
964    /// Runs one command as the app binary would, e.g. from a test or a
965    /// program that drives the app: `["migrate:status"]`, `["down",
966    /// "--secret", "abc"]`, or `[]` for `serve`. Output goes to stdout.
967    ///
968    /// ```no_run
969    /// # use renox::prelude::*;
970    /// # async fn demo() -> Result {
971    /// App::new().run_args(["migrate"]).await?;
972    /// # Ok(()) }
973    /// ```
974    pub async fn run_args(self, args: impl IntoIterator<Item = impl Into<String>>) -> Result {
975        let args: Vec<String> = args.into_iter().map(Into::into).collect();
976        self.run_command(&args).await
977    }
978
979    /// Starts the server on the current Tokio runtime.
980    pub async fn serve(self) -> Result {
981        self.run_command(&[]).await
982    }
983
984    async fn run_command(self, args: &[String]) -> Result {
985        let command = args.first().map(String::as_str).unwrap_or("serve");
986        if matches!(command, "help" | "--help" | "-h") {
987            // Modules add their commands in `register`, which boot runs.
988            let mut modules = Registry::default();
989            for module in &self.modules {
990                module.register(&mut modules);
991            }
992            let mut commands = self.registry.commands.clone();
993            commands.extend(modules.commands);
994            println!("{USAGE}{}", app_commands_help(&commands));
995            return Ok(());
996        }
997
998        let config = match self.config.clone() {
999            Some(config) => config,
1000            None => Config::load()?,
1001        };
1002        let long_running = matches!(command, "serve" | "queue:work" | "schedule:work");
1003        init_tracing(&config, long_running);
1004        if command == "serve" && config.key.is_none() && config.env != Environment::Testing {
1005            tracing::warn!(
1006                "APP_KEY is not set; using a temporary key, so sessions end on restart. \
1007                 Run `rnx key:generate`."
1008            );
1009        }
1010        let kernel = App {
1011            config: Some(config),
1012            ..self
1013        }
1014        .boot()
1015        .await?;
1016
1017        match command {
1018            "serve" => kernel.serve().await?,
1019            "migrate" => print_done("Migrated", &kernel.migrate().await?),
1020            "migrate:rollback" => {
1021                let steps = flag_value(args, "--step")?.unwrap_or(1);
1022                print_done("Rolled back", &kernel.rollback(steps).await?);
1023            }
1024            "migrate:fresh" => {
1025                println!("Dropped all tables.");
1026                print_done("Migrated", &kernel.fresh().await?);
1027                if args.iter().any(|a| a == "--seed") {
1028                    kernel.seed().await?;
1029                    println!("Seeded.");
1030                }
1031            }
1032            "db:diff" if !kernel.commands.iter().any(|c| c.name == "db:diff") => {
1033                crate::db::auto_migration::run(&kernel, &args[1..]).await?
1034            }
1035            "db:check" if !kernel.commands.iter().any(|c| c.name == "db:check") => {
1036                kernel.db_check().await?
1037            }
1038            "view:check" if !kernel.commands.iter().any(|c| c.name == "view:check") => {
1039                let problems = kernel.state.views.check(&kernel.state.routes);
1040                for problem in &problems {
1041                    println!("{problem}");
1042                }
1043                let checked = kernel.state.views.names().len();
1044                println!("Checked {checked} templates: {} problems.", problems.len());
1045                if !problems.is_empty() {
1046                    return Err(anyhow!("{} template problem(s)", problems.len()).into());
1047                }
1048            }
1049            "view:data" if !kernel.commands.iter().any(|c| c.name == "view:data") => {
1050                let path = std::path::Path::new(
1051                    flag_text(args, "--out").unwrap_or(".vscode/renox-components.json"),
1052                );
1053                if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
1054                    std::fs::create_dir_all(dir)?;
1055                }
1056                let mut text = serde_json::to_string_pretty(&kernel.state.views.custom_data())
1057                    .map_err(|e| anyhow!(e))?;
1058                text.push('\n');
1059                std::fs::write(path, text)?;
1060                println!("Wrote {}.", path.display());
1061            }
1062            "migrate:status" => {
1063                for m in kernel.migration_status().await? {
1064                    let note = if m.missing {
1065                        "  (applied, but its file is gone)"
1066                    } else if m.changed {
1067                        "  (edited after it ran; the edit won't run)"
1068                    } else {
1069                        ""
1070                    };
1071                    match m.batch {
1072                        Some(batch) => println!("  ran (batch {batch})  {}{note}", m.name),
1073                        None => println!("  pending          {}", m.name),
1074                    }
1075                }
1076            }
1077            "db:seed" => {
1078                kernel.seed().await?;
1079                println!("Seeded.");
1080            }
1081            "queue:work" => {
1082                let queues: Vec<String> = flag_text(args, "--queue")
1083                    .map(|q| q.split(',').map(|s| s.trim().to_owned()).collect())
1084                    .unwrap_or_default();
1085                let worker = kernel.worker(queues);
1086                if args.iter().any(|a| a == "--once") {
1087                    println!("Ran {} job(s).", worker.drain().await?);
1088                } else {
1089                    let workers = flag_value(args, "--workers")?.unwrap_or(1) as usize;
1090                    let (stop, stopped) = watch::channel(false);
1091                    let running = tokio::spawn(worker.run(workers, stopped));
1092                    shutdown_signal().await;
1093                    let _ = stop.send(true);
1094                    let _ = running.await;
1095                }
1096            }
1097            "webhook:failed" => {
1098                let failed = crate::webhook::WebhookCall::failed(&kernel.state.db).await?;
1099                if failed.is_empty() {
1100                    println!("No failed webhook calls.");
1101                }
1102                for call in failed {
1103                    println!(
1104                        "  #{} {} {}: {}",
1105                        call.id,
1106                        call.provider,
1107                        call.event_id,
1108                        call.error
1109                            .unwrap_or_default()
1110                            .lines()
1111                            .next()
1112                            .unwrap_or_default()
1113                    );
1114                }
1115            }
1116            "webhook:retry" => {
1117                let id: i64 = args
1118                    .get(1)
1119                    .and_then(|id| id.parse().ok())
1120                    .ok_or_else(|| anyhow!("usage: webhook:retry <id>"))?;
1121                if crate::webhook::retry(&kernel.state, id).await? {
1122                    println!("Webhook call #{id} queued again.");
1123                } else {
1124                    return Err(anyhow!("there is no webhook call #{id}").into());
1125                }
1126            }
1127            "queue:failed" => {
1128                let failed = kernel.state.queue.failed().await?;
1129                if failed.is_empty() {
1130                    println!("No failed jobs.");
1131                }
1132                for job in failed {
1133                    println!("  #{} {} ({}): {}", job.id, job.job, job.queue, job.error);
1134                }
1135            }
1136            "queue:retry" => {
1137                let id = match args.get(1).map(String::as_str) {
1138                    Some("all") => None,
1139                    Some(id) => Some(
1140                        id.parse()
1141                            .map_err(|_| anyhow!("expected a job id or `all`"))?,
1142                    ),
1143                    None => return Err(anyhow!("usage: queue:retry <id|all>").into()),
1144                };
1145                let queued = match id {
1146                    Some(id) => u64::from(kernel.state.queue.retry(id).await?),
1147                    None => kernel.state.queue.retry_all().await?,
1148                };
1149                println!("Queued {queued} job(s) again.");
1150            }
1151            "queue:forget" => {
1152                let id: i64 = args
1153                    .get(1)
1154                    .and_then(|id| id.parse().ok())
1155                    .ok_or_else(|| anyhow!("usage: queue:forget <id>"))?;
1156                if kernel.state.queue.forget_failed(id).await? {
1157                    println!("Deleted failed job {id}.");
1158                } else {
1159                    return Err(anyhow!("no failed job {id}").into());
1160                }
1161            }
1162            "queue:prune-failed" => {
1163                let hours = flag_value(args, "--hours")?.unwrap_or(168);
1164                let age = Duration::from_secs(u64::from(hours) * 3600);
1165                println!(
1166                    "Deleted {} failed job(s) older than {hours} hour(s).",
1167                    kernel.state.queue.prune_failed(age).await?
1168                );
1169            }
1170            "queue:prune-batches" => {
1171                let hours = flag_value(args, "--hours")?.unwrap_or(24);
1172                let age = Duration::from_secs(u64::from(hours) * 3600);
1173                println!(
1174                    "Deleted {} batch(es) finished more than {hours} hour(s) ago.",
1175                    kernel.state.queue.prune_batches(age).await?
1176                );
1177            }
1178            "queue:flush" => println!(
1179                "Deleted {} failed job(s).",
1180                kernel.state.queue.flush_failed().await?
1181            ),
1182            "ui:publish" => {
1183                let force = args.iter().any(|a| a == "--force");
1184                crate::assets::publish_ui(&kernel.state.config, force)?;
1185            }
1186            "session:prune" => println!(
1187                "Deleted {} expired session(s).",
1188                crate::Session::prune_expired(&kernel.state.db).await?
1189            ),
1190            "cache:prune" => println!(
1191                "Deleted {} expired cache row(s).",
1192                kernel.state.cache.prune().await?
1193            ),
1194            "schedule:list" => {
1195                if kernel.schedule.is_empty() {
1196                    println!("No scheduled tasks.");
1197                }
1198                for run in kernel.schedule.upcoming(kernel.zone) {
1199                    let when = match run.at {
1200                        Some(at) => run
1201                            .zone
1202                            .local(at.timestamp())
1203                            .format("%Y-%m-%d %H:%M")
1204                            .to_string(),
1205                        None => "never".to_owned(),
1206                    };
1207                    println!("  {when:<16}  {:<18}  {}", run.zone, run.name);
1208                }
1209            }
1210            "schedule:run" => {
1211                let Some(name) = args.get(1).filter(|a| !a.starts_with('-')) else {
1212                    return Err(anyhow!("usage: schedule:run <task>").into());
1213                };
1214                kernel.run_scheduled(name).await?;
1215                println!("Ran `{name}`.");
1216            }
1217            "schedule:work" => {
1218                let (stop, stopped) = watch::channel(false);
1219                let running = tokio::spawn(kernel.schedule.clone().run(
1220                    kernel.state.clone(),
1221                    kernel.zone,
1222                    stopped,
1223                ));
1224                shutdown_signal().await;
1225                let _ = stop.send(true);
1226                let _ = running.await;
1227            }
1228            "route:list" => print_routes(kernel.routes()),
1229            "db:shell" => crate::shell::run(kernel.db()).await?,
1230            "down" => {
1231                let secret = flag_text(args, "--secret").map(str::to_owned);
1232                let retry = flag_value(args, "--retry")?.map(u64::from);
1233                let mut options = crate::maintenance::DownOptions::new();
1234                if let Some(secret) = &secret {
1235                    options = options.secret(secret.clone());
1236                }
1237                if let Some(retry) = retry {
1238                    options = options.retry(retry);
1239                }
1240                crate::maintenance::down(&kernel.state.config.storage_path, options)?;
1241                match secret {
1242                    Some(secret) => println!("The app is down. Visit /{secret} to bypass it."),
1243                    None => println!("The app is down."),
1244                }
1245            }
1246            "up" => match crate::maintenance::up(&kernel.state.config.storage_path)? {
1247                true => println!("The app is up."),
1248                false => println!("The app was not down."),
1249            },
1250            other if kernel.commands.iter().any(|c| c.name == other) => {
1251                kernel.call(other, args[1..].iter().cloned()).await?;
1252            }
1253            other => {
1254                return Err(anyhow!(
1255                    "unknown command `{other}`\n\n{USAGE}{}",
1256                    app_commands_help(&kernel.commands)
1257                )
1258                .into());
1259            }
1260        }
1261        Ok(())
1262    }
1263}
1264
1265/// The listening socket systemd passes with socket activation
1266/// (`LISTEN_FDS`, a `.socket` unit): it stays open while the service
1267/// restarts, so connections wait in its queue instead of being refused.
1268fn inherited_listener() -> Result<Option<TcpListener>> {
1269    let mut fds = listenfd::ListenFd::from_env();
1270    let Some(listener) = fds
1271        .take_tcp_listener(0)
1272        .map_err(|err| anyhow!("the socket systemd passed isn't a TCP listener: {err}"))?
1273    else {
1274        return Ok(None);
1275    };
1276    listener
1277        .set_nonblocking(true)
1278        .map_err(anyhow::Error::from)?;
1279    Ok(Some(
1280        TcpListener::from_std(listener).map_err(anyhow::Error::from)?,
1281    ))
1282}
1283
1284/// Built-in commands; an app command can't take one of these names.
1285///
1286/// The list is the 1.0 set and stays as it is through 1.x (docs/stability.md):
1287/// refusing a name an app already uses would stop that app from booting. A
1288/// command added in 1.x must let an app command of the same name run in its
1289/// place instead of joining this list.
1290const BUILT_IN_COMMANDS: &[&str] = &[
1291    "serve",
1292    "migrate",
1293    "migrate:rollback",
1294    "migrate:fresh",
1295    "migrate:status",
1296    "db:seed",
1297    "queue:work",
1298    "queue:failed",
1299    "queue:retry",
1300    "queue:flush",
1301    "queue:forget",
1302    "queue:prune-failed",
1303    "queue:prune-batches",
1304    "webhook:failed",
1305    "webhook:retry",
1306    "cache:prune",
1307    "session:prune",
1308    "ui:publish",
1309    "schedule:list",
1310    "schedule:run",
1311    "schedule:work",
1312    "route:list",
1313    "db:shell",
1314    "down",
1315    "up",
1316    "help",
1317];
1318
1319fn check_commands(commands: &[crate::command::Command]) -> Result {
1320    let mut seen = std::collections::HashSet::new();
1321    for command in commands {
1322        let name = command.name.as_str();
1323        if name.is_empty() || name.starts_with('-') || name.contains(char::is_whitespace) {
1324            return Err(anyhow!("`{name}` is not a valid command name").into());
1325        }
1326        if BUILT_IN_COMMANDS.contains(&name) {
1327            return Err(anyhow!("the command `{name}` is built in; choose another name").into());
1328        }
1329        if !seen.insert(name) {
1330            return Err(anyhow!("the command `{name}` is registered twice").into());
1331        }
1332    }
1333    Ok(())
1334}
1335
1336fn app_commands_help(commands: &[crate::command::Command]) -> String {
1337    if commands.is_empty() {
1338        return String::new();
1339    }
1340    let mut out = String::from("\n\nApp commands:\n");
1341    for command in commands {
1342        out.push_str(&format!("  {:<26}{}\n", command.name, command.about));
1343    }
1344    out.trim_end().to_owned()
1345}
1346
1347impl Default for App {
1348    fn default() -> Self {
1349        Self::new()
1350    }
1351}
1352
1353/// A booted application: its router, database, queue and maintenance commands.
1354pub struct Kernel {
1355    router: Router,
1356    listing: Vec<RouteInfo>,
1357    state: AppState,
1358    migrator: Migrator,
1359    seeders: Vec<crate::registry::Seeder>,
1360    models: Vec<crate::db::schema::ModelInfo>,
1361    handlers: Handlers,
1362    schedule: Schedule,
1363    zone: Zone,
1364    commands: Vec<crate::command::Command>,
1365}
1366
1367impl Kernel {
1368    /// A scratch database with every migration run (reused by #371).
1369    pub(crate) async fn scratch_db(&self) -> Result<Db> {
1370        let db = crate::db::connect_scratch(&self.state.config).await?;
1371        self.migrator.run(&db).await?;
1372        Ok(db)
1373    }
1374
1375    /// `db:check`: compares the models with what the migrations build.
1376    async fn db_check(&self) -> Result {
1377        let db = self.scratch_db().await?;
1378        let reports = crate::db::schema_check::check(&db, &self.models).await;
1379        crate::db::drop_scratch(&db).await;
1380        let (text, problems) = crate::db::schema_check::render(&reports?);
1381        print!("{text}");
1382        if problems > 0 {
1383            return Err(anyhow::anyhow!("{problems} problem(s) in the models").into());
1384        }
1385        Ok(())
1386    }
1387
1388    /// The models registered with `App::model`, once each.
1389    pub(crate) fn models(&self) -> &[crate::db::schema::ModelInfo] {
1390        &self.models
1391    }
1392
1393    /// Runs the app command `name` (see [`App::command`]), e.g. from a test.
1394    pub async fn call(
1395        &self,
1396        name: &str,
1397        args: impl IntoIterator<Item = impl Into<String>>,
1398    ) -> Result {
1399        let command = self
1400            .commands
1401            .iter()
1402            .find(|c| c.name == name)
1403            .ok_or_else(|| anyhow!("unknown command `{name}`"))?;
1404        let run = (command.run)(self.state.clone(), crate::command::Args::new(args));
1405        crate::context::scope_app(self.state.clone(), run).await
1406    }
1407
1408    /// Runs the scheduled task `name` now, with its hooks (what
1409    /// `schedule:run` does). In a test, `TestApp::run_scheduled` does the same
1410    /// at the time `TestApp::travel` moved the clock to.
1411    pub async fn run_scheduled(&self, name: &str) -> Result {
1412        self.schedule.run_now(self.state.clone(), name).await
1413    }
1414
1415    /// The assembled router, e.g. to drive with `tower::ServiceExt::oneshot`.
1416    pub fn router(&self) -> Router {
1417        self.router.clone()
1418    }
1419
1420    /// Every route, sorted by path, as `route:list` prints them.
1421    pub fn routes(&self) -> &[RouteInfo] {
1422        &self.listing
1423    }
1424
1425    /// What handlers get as `State<AppState>`.
1426    pub fn state(&self) -> &AppState {
1427        &self.state
1428    }
1429
1430    /// The database connection pool.
1431    pub fn db(&self) -> &Db {
1432        &self.state.db
1433    }
1434
1435    /// The mailer; with `MAIL_MAILER=memory`, `mailer().sent()` lists what was sent.
1436    pub fn mailer(&self) -> &Mailer {
1437        &self.state.mailer
1438    }
1439
1440    /// A worker for the given queues (all queues when empty).
1441    pub fn worker(&self, queues: Vec<String>) -> Worker {
1442        Worker::new(self.state.clone(), self.handlers.clone(), queues)
1443    }
1444
1445    /// Runs every job that is available now, e.g. in tests; returns how many ran.
1446    pub async fn run_jobs(&self) -> Result<usize> {
1447        self.worker(Vec::new()).drain().await
1448    }
1449
1450    /// Serves HTTP with queue workers and the scheduler in the same process,
1451    /// until Ctrl-C or SIGTERM; then lets running jobs finish.
1452    pub async fn serve(self) -> Result {
1453        let config = self.state.config.clone();
1454        let listener = match inherited_listener()? {
1455            Some(listener) => {
1456                tracing::info!("using the socket systemd passed (socket activation)");
1457                listener
1458            }
1459            None => TcpListener::bind(config.addr()).await?,
1460        };
1461        tracing::info!(
1462            "{} listening on http://{}",
1463            config.name,
1464            listener.local_addr()?
1465        );
1466
1467        let (stop, stopped) = watch::channel(false);
1468        let mut background = Vec::new();
1469        if config.queue_workers > 0 {
1470            tracing::info!(workers = config.queue_workers, "queue workers started");
1471            let worker = self.worker(Vec::new());
1472            background.push(tokio::spawn(
1473                worker.run(config.queue_workers, stopped.clone()),
1474            ));
1475        }
1476        if config.scheduler && !self.schedule.is_empty() {
1477            tracing::info!("scheduler started");
1478            background.push(tokio::spawn(self.schedule.clone().run(
1479                self.state.clone(),
1480                self.zone,
1481                stopped,
1482            )));
1483        }
1484
1485        let service = self
1486            .router
1487            .into_make_service_with_connect_info::<SocketAddr>();
1488        let live = self.state.live.clone();
1489        let notification_hub = self.state.notification_hub.clone();
1490        axum::serve(listener, service)
1491            .with_graceful_shutdown(async move {
1492                shutdown_signal().await;
1493                // Open live-reload and notification streams would otherwise
1494                // hold the shutdown.
1495                if let Some(live) = live {
1496                    live.stop();
1497                }
1498                notification_hub.stop();
1499            })
1500            .await?;
1501
1502        let _ = stop.send(true);
1503        let finished = async {
1504            for task in background {
1505                let _ = task.await;
1506            }
1507        };
1508        if tokio::time::timeout(SHUTDOWN_GRACE, finished)
1509            .await
1510            .is_err()
1511        {
1512            tracing::warn!("background work was still running after {SHUTDOWN_GRACE:?}");
1513        }
1514        tracing::info!("{} stopped", config.name);
1515        Ok(())
1516    }
1517
1518    /// Runs pending migrations; returns their names.
1519    pub async fn migrate(&self) -> Result<Vec<String>> {
1520        let done = self.migrator.run(self.db()).await?;
1521        Ok(done.into_iter().map(str::to_owned).collect())
1522    }
1523
1524    /// Undoes the last `batches` batches of migrations; returns their names.
1525    pub async fn rollback(&self, batches: u32) -> Result<Vec<String>> {
1526        Ok(self.migrator.rollback(self.db(), batches).await?)
1527    }
1528
1529    /// Drops every table and runs all migrations.
1530    pub async fn fresh(&self) -> Result<Vec<String>> {
1531        let done = self.migrator.fresh(self.db()).await?;
1532        Ok(done.into_iter().map(str::to_owned).collect())
1533    }
1534
1535    /// Every migration and whether it has run, as `migrate:status` prints them.
1536    pub async fn migration_status(&self) -> Result<Vec<MigrationStatus>> {
1537        Ok(self.migrator.status(self.db()).await?)
1538    }
1539
1540    /// Runs every seeder in registration order.
1541    pub async fn seed(&self) -> Result {
1542        for seeder in &self.seeders {
1543            // In the app's context, so a seeder can reach `renox::context::app()`
1544            // (config, `encrypt`, the cache) and model hooks see it too.
1545            crate::context::scope_app(self.state.clone(), seeder(self.state.clone())).await?;
1546        }
1547        Ok(())
1548    }
1549}
1550
1551/// Routes Renox adds itself.
1552fn framework_routes(config: &Config) -> Vec<RouteInfo> {
1553    let route = |method: &str, path: &str| RouteInfo {
1554        method: method.to_owned(),
1555        path: path.to_owned(),
1556        name: None,
1557        module: "renox".to_owned(),
1558        middleware: Vec::new(),
1559        domain: None,
1560    };
1561    let mut routes = vec![
1562        route("GET", "/health"),
1563        route("GET", "/robots.txt"),
1564        route("GET", "/favicon.ico"),
1565        route("GET", "/_renox/{asset}"),
1566        route("GET", "/_renox/files/{*key}"),
1567        route("POST", "/_renox/grid/{grid}/prefs"),
1568        route("DELETE", "/_renox/grid/{grid}/prefs"),
1569        route("POST", "/_renox/live/{component}/{action}"),
1570        route("GET", "/storage/{*path}"),
1571    ];
1572    if config.debug {
1573        routes.push(route("GET", "/_renox/mail"));
1574        routes.push(route("GET", "/_renox/mail/{id}"));
1575    }
1576    if config.debug && config.env == Environment::Local {
1577        routes.push(route("GET", "/_renox/live"));
1578    }
1579    routes
1580}
1581
1582fn print_routes(routes: &[RouteInfo]) {
1583    print!("{}", route_table(routes));
1584}
1585
1586/// `route:list`'s table.
1587fn route_table(routes: &[RouteInfo]) -> String {
1588    use std::fmt::Write as _;
1589    let mut out = String::new();
1590    let with_domains = routes.iter().any(|r| r.domain.is_some());
1591    let rows: Vec<Vec<String>> = routes
1592        .iter()
1593        .map(|r| {
1594            let mut row = vec![
1595                r.method.clone(),
1596                r.path.clone(),
1597                r.name.clone().unwrap_or_default(),
1598                r.module.clone(),
1599                r.middleware
1600                    .iter()
1601                    .map(|guard| {
1602                        if is_role_guard(guard) {
1603                            format!("{guard}*")
1604                        } else {
1605                            guard.clone()
1606                        }
1607                    })
1608                    .collect::<Vec<_>>()
1609                    .join(", "),
1610            ];
1611            if with_domains {
1612                row.insert(0, r.domain.clone().unwrap_or_default());
1613            }
1614            row
1615        })
1616        .collect();
1617    let mut header: Vec<String> = ["METHOD", "PATH", "NAME", "MODULE", "MIDDLEWARE"]
1618        .map(str::to_owned)
1619        .to_vec();
1620    if with_domains {
1621        header.insert(0, "DOMAIN".into());
1622    }
1623    let mut widths: Vec<usize> = header.iter().map(String::len).collect();
1624    for row in &rows {
1625        for (width, cell) in widths.iter_mut().zip(row) {
1626            *width = (*width).max(cell.chars().count());
1627        }
1628    }
1629    for row in std::iter::once(&header).chain(&rows) {
1630        let line: Vec<String> = row
1631            .iter()
1632            .zip(&widths)
1633            .map(|(cell, width)| format!("{cell:<width$}", width = *width))
1634            .collect();
1635        let _ = writeln!(out, "{}", line.join("  ").trim_end());
1636    }
1637    if routes
1638        .iter()
1639        .flat_map(|r| &r.middleware)
1640        .any(|g| is_role_guard(g))
1641    {
1642        out.push_str(
1643            "\n* role and permission guards count the user's global roles plus the roles \
1644             given in the request's active scope (permissions::set_scope).\n",
1645        );
1646    }
1647    out
1648}
1649
1650/// A `role:` or `permission:` guard, which counts scoped roles too.
1651fn is_role_guard(guard: &str) -> bool {
1652    guard.starts_with("role:") || guard.starts_with("permission:")
1653}
1654
1655fn print_done(verb: &str, names: &[String]) {
1656    if names.is_empty() {
1657        println!("Nothing to do.");
1658    }
1659    for name in names {
1660        println!("{verb}: {name}");
1661    }
1662}
1663
1664fn flag_text<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
1665    let i = args.iter().position(|a| a == flag)?;
1666    args.get(i + 1).map(String::as_str)
1667}
1668
1669fn flag_value(args: &[String], flag: &str) -> Result<Option<u32>> {
1670    if !args.iter().any(|a| a == flag) {
1671        return Ok(None);
1672    }
1673    match flag_text(args, flag).and_then(|v| v.parse().ok()) {
1674        Some(value) => Ok(Some(value)),
1675        None => Err(anyhow!("{flag} needs a number").into()),
1676    }
1677}
1678
1679/// Turns a handler that panics or runs past `REQUEST_TIMEOUT` into a 500
1680/// with the error page, instead of a dropped connection or a hung client.
1681async fn guard(
1682    limit: Option<Duration>,
1683    req: axum::extract::Request,
1684    next: axum::middleware::Next,
1685) -> axum::response::Response {
1686    use axum::response::IntoResponse;
1687    let run = futures_util::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(next.run(req)));
1688    let outcome = match limit {
1689        Some(limit) => match tokio::time::timeout(limit, run).await {
1690            Ok(outcome) => outcome,
1691            Err(_) => {
1692                return Error::Internal(anyhow!(
1693                    "the request took longer than REQUEST_TIMEOUT ({}s)",
1694                    limit.as_secs()
1695                ))
1696                .into_response();
1697            }
1698        },
1699        None => run.await,
1700    };
1701    outcome.unwrap_or_else(|panic| {
1702        let message = crate::error::panic_message(&*panic);
1703        Error::Internal(anyhow!("the handler panicked: {message}")).into_response()
1704    })
1705}
1706
1707/// How much longer than `REQUEST_TIMEOUT` the outer limit (`backstop`) waits,
1708/// so a slow handler still gets the inner one's error page.
1709const BACKSTOP_GRACE: Duration = Duration::from_secs(1);
1710
1711/// `REQUEST_TIMEOUT` around the session, i18n, auth, CSRF and view layers:
1712/// `guard` only covers the handler, so a hang before it (database sessions
1713/// on a database that stopped answering) would otherwise go unbounded.
1714/// Answers a plain 500 a little after the handler's own limit.
1715async fn backstop(
1716    limit: Option<Duration>,
1717    req: axum::extract::Request,
1718    next: axum::middleware::Next,
1719) -> axum::response::Response {
1720    use axum::response::IntoResponse;
1721    let Some(limit) = limit else {
1722        return next.run(req).await;
1723    };
1724    match tokio::time::timeout(limit + BACKSTOP_GRACE, next.run(req)).await {
1725        Ok(res) => res,
1726        Err(_) => Error::Internal(anyhow!(
1727            "the request took longer than REQUEST_TIMEOUT ({}s) before reaching its handler",
1728            limit.as_secs()
1729        ))
1730        .into_response(),
1731    }
1732}
1733
1734/// Adds a module's routes; what axum refuses to merge is a boot error.
1735fn merge_routes(
1736    router: Router<AppState>,
1737    other: Router<AppState>,
1738    module: &str,
1739) -> Result<Router<AppState>> {
1740    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| router.merge(other))).map_err(
1741        |panic| {
1742            let message = crate::error::panic_message(&*panic);
1743            anyhow!("the routes of the `{module}` module clash: {message}").into()
1744        },
1745    )
1746}
1747
1748/// A route of `module` that another module defines already (on the same
1749/// domain, or both without one).
1750fn check_clashes(
1751    listing: &[RouteInfo],
1752    new: &[RouteInfo],
1753    domain: Option<&str>,
1754    module: &str,
1755) -> Result {
1756    for info in new {
1757        let clash = listing.iter().find(|other| {
1758            other.domain.as_deref() == domain
1759                && other.path == info.path
1760                && (other.method == info.method || other.method == "*" || info.method == "*")
1761        });
1762        if let Some(other) = clash {
1763            let on = domain.map(|d| format!(" on {d}")).unwrap_or_default();
1764            return Err(anyhow!(
1765                "{} {}{on} is defined by both the `{}` and the `{module}` module",
1766                info.method,
1767                info.path,
1768                other.module,
1769            )
1770            .into());
1771        }
1772    }
1773    Ok(())
1774}
1775
1776/// `Registry::asset` paths: each starts with `/` and is used once.
1777fn check_assets(assets: &[crate::registry::StaticAsset]) -> Result<()> {
1778    let mut seen = std::collections::HashSet::new();
1779    for asset in assets {
1780        if !asset.path.starts_with('/') || asset.path.contains(['{', '}']) {
1781            return Err(anyhow!(
1782                "Registry::asset(\"{}\", …): the path must start with `/` and have no `{{…}}` parameters",
1783                asset.path
1784            )
1785            .into());
1786        }
1787        if !seen.insert(asset.path) {
1788            return Err(anyhow!(
1789                "two modules serve a file at `{}` (Registry::asset)",
1790                asset.path
1791            )
1792            .into());
1793        }
1794    }
1795    Ok(())
1796}
1797
1798fn build_router(
1799    router: Router<AppState>,
1800    state: AppState,
1801    embedded_public: Option<&'static [(&'static str, &'static [u8])]>,
1802    fallback: Option<axum::routing::MethodRouter<AppState>>,
1803    static_assets: &[crate::registry::StaticAsset],
1804) -> Router {
1805    // No route and no public file: the app's fallback (`Routes::fallback`), else a 404.
1806    let fallback = fallback.map(|handler| handler.with_state(state.clone()));
1807    let not_found = move |req: axum::extract::Request| {
1808        let fallback = fallback.clone();
1809        async move {
1810            match fallback {
1811                Some(handler) => match tower::ServiceExt::oneshot(handler, req).await {
1812                    Ok(res) => res,
1813                    Err(never) => match never {},
1814                },
1815                None => axum::response::IntoResponse::into_response(Error::NotFound),
1816            }
1817        }
1818    };
1819    let router = router
1820        .merge(crate::storage::router())
1821        .merge(crate::grid::router())
1822        .merge(crate::live_component::route::router());
1823    let router = if state.config.debug {
1824        router
1825            .merge(crate::mail::preview_router())
1826            .merge(crate::inspector::router())
1827    } else {
1828        router
1829    };
1830    let public = state.config.public_path.clone();
1831    let router = if let Some(files) = embedded_public {
1832        let files = crate::embedded::public_map(files);
1833        let not_found = not_found.clone();
1834        router.fallback(move |req: axum::extract::Request| {
1835            let (files, not_found) = (files.clone(), not_found.clone());
1836            async move {
1837                let res = crate::embedded::serve(&files, req.uri());
1838                if res.status() == axum::http::StatusCode::NOT_FOUND {
1839                    not_found(req).await
1840                } else {
1841                    res
1842                }
1843            }
1844        })
1845    } else if public.is_dir() {
1846        // `fallback`, not `not_found_service`: the latter makes every answer
1847        // a 404, and an app's fallback may redirect or answer 200.
1848        let files = ServeDir::new(public).fallback(not_found.clone().into_service());
1849        router.fallback(move |req: axum::extract::Request| {
1850            let files = files.clone();
1851            async move {
1852                // A name no file system accepts (over 255 bytes) can't be a
1853                // file here; asking the OS would be a "name too long" 500.
1854                if req
1855                    .uri()
1856                    .path()
1857                    .split('/')
1858                    .any(|segment| segment.len() > 255)
1859                {
1860                    return axum::response::IntoResponse::into_response(Error::NotFound);
1861                }
1862                let versioned = crate::embedded::is_versioned(req.uri());
1863                match tower::ServiceExt::oneshot(files, req).await {
1864                    Ok(res) => {
1865                        let mut res = axum::response::IntoResponse::into_response(res);
1866                        if versioned && res.status().is_success() {
1867                            res.headers_mut().insert(
1868                                axum::http::header::CACHE_CONTROL,
1869                                axum::http::HeaderValue::from_static(
1870                                    "public, max-age=31536000, immutable",
1871                                ),
1872                            );
1873                        }
1874                        res
1875                    }
1876                    Err(never) => match never {},
1877                }
1878            }
1879        })
1880    } else {
1881        router.fallback(not_found)
1882    };
1883
1884    let request_timeout = state.config.request_timeout;
1885    let router: Router = router
1886        .layer(from_fn(
1887            move |req: axum::extract::Request, next: axum::middleware::Next| {
1888                guard(request_timeout, req, next)
1889            },
1890        ))
1891        .layer(from_fn_with_state(
1892            state.clone(),
1893            crate::maintenance::middleware,
1894        ))
1895        .layer(from_fn_with_state(state.clone(), view::middleware))
1896        .layer(from_fn(csrf::middleware))
1897        .layer(from_fn_with_state(state.clone(), auth::middleware))
1898        .layer(from_fn_with_state(state.clone(), crate::i18n::middleware))
1899        .layer(from_fn_with_state(state.clone(), session::middleware))
1900        // Each request's own `renox::context`, around everything the app runs.
1901        .layer(from_fn_with_state(
1902            state.clone(),
1903            crate::context::middleware,
1904        ))
1905        .layer(from_fn(
1906            move |req: axum::extract::Request, next: axum::middleware::Next| {
1907                backstop(request_timeout, req, next)
1908            },
1909        ))
1910        // `/_renox/debug`: around it all, so the session's and user's SQL count.
1911        .layer(from_fn_with_state(
1912            state.clone(),
1913            crate::inspector::middleware,
1914        ))
1915        .merge(assets::router())
1916        .merge(assets::module_router(static_assets))
1917        .merge(crate::health::router())
1918        .merge(robots(&state, embedded_public))
1919        .merge(favicon(&state, embedded_public))
1920        .merge(crate::live::router())
1921        .merge(public_files(&state))
1922        .layer(axum::extract::DefaultBodyLimit::max(
1923            state.config.upload_max_size,
1924        ))
1925        .layer(
1926            TraceLayer::new_for_http().make_span_with(|req: &axum::extract::Request| {
1927                let ip = crate::ClientIp::of(req).map(|ip| ip.to_string());
1928                let id = req
1929                    .headers()
1930                    .get(crate::request_id::HEADER)
1931                    .and_then(|v| v.to_str().ok())
1932                    .unwrap_or_default();
1933                tracing::info_span!(
1934                    "request",
1935                    id = %id,
1936                    method = %req.method(),
1937                    uri = %req.uri(),
1938                    ip = ip.as_deref().unwrap_or("unknown"),
1939                )
1940            }),
1941        )
1942        .layer(from_fn(crate::request_id::middleware))
1943        .with_state(state.clone());
1944    // Method spoofing must change the method before the router matches it.
1945    let limit = state.config.upload_max_size;
1946    let spoofing = from_fn(
1947        move |req: axum::extract::Request, next: axum::middleware::Next| {
1948            crate::method::middleware(req, next, limit)
1949        },
1950    );
1951    // Security headers outermost, so every response gets them, including
1952    // the method-spoofing layer's own 413.
1953    Router::new()
1954        .fallback_service(tower::Layer::layer(&spoofing, router))
1955        .layer(from_fn_with_state(state, crate::security::middleware))
1956}
1957
1958/// Whether the app ships `name` in `public/` (on disk, or embedded).
1959fn has_public_file(
1960    state: &AppState,
1961    embedded_public: Option<&'static [(&'static str, &'static [u8])]>,
1962    name: &str,
1963) -> bool {
1964    match embedded_public {
1965        Some(files) => files.iter().any(|(path, _)| *path == name),
1966        None => state.config.public_path.join(name).is_file(),
1967    }
1968}
1969
1970/// The generated `/robots.txt`, unless the app ships its own in `public/`.
1971fn robots(
1972    state: &AppState,
1973    embedded_public: Option<&'static [(&'static str, &'static [u8])]>,
1974) -> Router<AppState> {
1975    if has_public_file(state, embedded_public, "robots.txt") {
1976        Router::new()
1977    } else {
1978        crate::seo::robots_router(state)
1979    }
1980}
1981
1982/// `/favicon.ico` answers 204 (no icon, cached for a day) unless the app
1983/// ships one in `public/`: browsers ask for it on every site, and a 404
1984/// would run the whole stack, render the error page and log it each time.
1985fn favicon(
1986    state: &AppState,
1987    embedded_public: Option<&'static [(&'static str, &'static [u8])]>,
1988) -> Router<AppState> {
1989    if has_public_file(state, embedded_public, "favicon.ico") {
1990        return Router::new();
1991    }
1992    Router::new().route(
1993        "/favicon.ico",
1994        axum::routing::get(|| async {
1995            (
1996                axum::http::StatusCode::NO_CONTENT,
1997                [(axum::http::header::CACHE_CONTROL, "public, max-age=86400")],
1998            )
1999        }),
2000    )
2001}
2002
2003/// Public files of the local disk at `/storage/...`, outside sessions.
2004fn public_files(state: &AppState) -> Router<AppState> {
2005    match state.storage.public_root() {
2006        Some(root) => Router::new()
2007            .nest_service("/storage", ServeDir::new(root))
2008            .layer(axum::middleware::map_response(user_file_headers)),
2009        None => Router::new(),
2010    }
2011}
2012
2013/// Uploaded files are other people's content served from the app's origin:
2014/// a sandbox CSP keeps any HTML or SVG among them from running scripts,
2015/// `nosniff` stops browsers guessing a type, and documents are downloaded
2016/// instead of displayed.
2017pub(crate) async fn user_file_headers(
2018    mut res: axum::response::Response,
2019) -> axum::response::Response {
2020    use axum::http::HeaderValue;
2021    use axum::http::header::{
2022        CONTENT_DISPOSITION, CONTENT_SECURITY_POLICY, CONTENT_TYPE, X_CONTENT_TYPE_OPTIONS,
2023    };
2024
2025    let content_type = res
2026        .headers()
2027        .get(CONTENT_TYPE)
2028        .and_then(|v| v.to_str().ok())
2029        .unwrap_or_default()
2030        .to_ascii_lowercase();
2031    let headers = res.headers_mut();
2032    headers.insert(
2033        CONTENT_SECURITY_POLICY,
2034        HeaderValue::from_static(
2035            "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox",
2036        ),
2037    );
2038    headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
2039    let document = ["html", "xml", "javascript", "ecmascript"]
2040        .iter()
2041        .any(|kind| content_type.contains(kind))
2042        && !content_type.contains("svg");
2043    if document {
2044        headers.insert(CONTENT_DISPOSITION, HeaderValue::from_static("attachment"));
2045    }
2046    res
2047}
2048
2049/// Long-running commands log at info; others only log warnings and errors.
2050fn init_tracing(config: &Config, long_running: bool) {
2051    let default = match (long_running, config.debug) {
2052        (true, true) => "info,renox=debug",
2053        (true, false) => "info",
2054        (false, _) => "warn",
2055    };
2056    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default));
2057    // LOG_FILE appends to a file (rotate it with logrotate, or leave logs to
2058    // journald/Docker on stdout); LOG_FORMAT=json writes one JSON object per
2059    // line, with the request span's fields (id, method, uri, ip).
2060    let file = config.log_file.as_ref().and_then(|path| {
2061        match std::fs::OpenOptions::new()
2062            .create(true)
2063            .append(true)
2064            .open(path)
2065        {
2066            Ok(file) => Some(std::sync::Mutex::new(file)),
2067            Err(err) => {
2068                eprintln!("LOG_FILE {}: {err}; logging to stdout", path.display());
2069                None
2070            }
2071        }
2072    });
2073    let json = config.log_format == crate::LogFormat::Json;
2074    let builder = tracing_subscriber::fmt().with_env_filter(filter);
2075    let _ = match (json, file) {
2076        (true, Some(file)) => builder
2077            .json()
2078            .with_current_span(true)
2079            .with_span_list(false)
2080            .with_writer(file)
2081            .try_init(),
2082        (true, None) => builder
2083            .json()
2084            .with_current_span(true)
2085            .with_span_list(false)
2086            .try_init(),
2087        (false, Some(file)) => builder.with_ansi(false).with_writer(file).try_init(),
2088        (false, None) => builder.try_init(),
2089    };
2090}
2091
2092async fn shutdown_signal() {
2093    let ctrl_c = async {
2094        let _ = tokio::signal::ctrl_c().await;
2095    };
2096
2097    #[cfg(unix)]
2098    let terminate = async {
2099        match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
2100            Ok(mut signal) => {
2101                signal.recv().await;
2102            }
2103            Err(_) => std::future::pending().await,
2104        }
2105    };
2106    #[cfg(not(unix))]
2107    let terminate = std::future::pending::<()>();
2108
2109    tokio::select! {
2110        _ = ctrl_c => {},
2111        _ = terminate => {},
2112    }
2113    tracing::info!("shutdown signal received");
2114}
2115
2116#[cfg(test)]
2117mod tests {
2118    use super::*;
2119
2120    fn route(path: &str, middleware: &[&str]) -> RouteInfo {
2121        RouteInfo {
2122            method: "GET".into(),
2123            path: path.into(),
2124            name: None,
2125            module: "app".into(),
2126            middleware: middleware.iter().map(|m| (*m).to_owned()).collect(),
2127            domain: None,
2128        }
2129    }
2130
2131    #[test]
2132    fn route_list_says_role_guards_count_the_active_scope() {
2133        let plain = route_table(&[route("/", &["auth"])]);
2134        assert!(!plain.contains('*'), "{plain}");
2135        let table = route_table(&[
2136            route("/orders", &["auth", "permission:orders.view"]),
2137            route("/drafts", &["role:editor"]),
2138            route("/admin", &["gate:admin"]),
2139        ]);
2140        assert!(table.contains("auth, permission:orders.view*"), "{table}");
2141        assert!(table.contains("role:editor*"), "{table}");
2142        assert!(table.contains("gate:admin\n"), "{table}");
2143        assert!(
2144            table.contains("active scope (permissions::set_scope)"),
2145            "{table}"
2146        );
2147    }
2148
2149    struct Note;
2150    impl crate::db::FromRow for Note {
2151        fn from_row(_: &crate::db::Row) -> Result<Self, crate::db::DbError> {
2152            Ok(Note)
2153        }
2154    }
2155    impl crate::db::Model for Note {
2156        const TABLE: &'static str = "notes";
2157        const COLUMNS: &'static [&'static str] = &["id"];
2158        type Key = i64;
2159        fn id(&self) -> i64 {
2160            0
2161        }
2162        fn set_id(&mut self, _: i64) {}
2163        fn values(&self) -> Vec<crate::db::DbValue> {
2164            Vec::new()
2165        }
2166    }
2167
2168    struct Registers;
2169    impl Module for Registers {
2170        fn name(&self) -> &'static str {
2171            "registers"
2172        }
2173        fn register(&self, app: &mut Registry) {
2174            app.model::<crate::auth::User>();
2175        }
2176    }
2177
2178    #[tokio::test]
2179    async fn models_are_kept_once_from_the_app_and_modules() {
2180        let boot = |app: App| async move { app.config(Config::default()).boot().await.unwrap() };
2181        let one = boot(
2182            App::new()
2183                .model::<crate::auth::User>()
2184                .model::<crate::auth::User>()
2185                .module(Registers),
2186        )
2187        .await;
2188        assert_eq!(one.models().len(), 1);
2189        assert_eq!(one.models()[0].table, "users");
2190        let two = boot(
2191            App::new()
2192                .model::<crate::auth::User>()
2193                .model::<Note>()
2194                .module(Registers),
2195        )
2196        .await;
2197        assert_eq!(two.models().len(), 2);
2198    }
2199}