Skip to main content

codoseo_web/
state.rs

1//! Shared state handed to every handler.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use sqlx::PgPool;
7use sqlx::postgres::PgPoolOptions;
8
9use codoseo_core::crawl::AddressPolicy;
10use codoseo_notify::{ChannelKey, GuardedHttp};
11
12use crate::auth::mailer::Mailer;
13use crate::config::{Config, Mode};
14
15#[derive(Clone)]
16pub struct AppState {
17    pub pool: PgPool,
18    pub config: Arc<Config>,
19    pub mailer: Mailer,
20    /// For outgoing calls (GitHub OAuth).
21    pub http: reqwest::Client,
22    /// Encrypts and decrypts alert channel targets (derived from `SECRET_KEY`).
23    pub channel_key: ChannelKey,
24    /// For "Send test" and for checking a webhook address when it is saved: the same guarded
25    /// client the job runner delivers with (the cloud refuses private and internal addresses,
26    /// self-hosted allows them).
27    pub notify_http: GuardedHttp,
28    /// Cancelled when the server begins shutting down ([`crate::serve`]), so long-lived
29    /// connections that axum's graceful shutdown would wait for (`/mcp` calls) end.
30    pub shutdown: tokio_util::sync::CancellationToken,
31    /// Calls per minute of the no-key MCP tools, per client address.
32    pub anon_calls: Arc<crate::agent::limiter::CallLimiter>,
33}
34
35impl AppState {
36    pub fn new(pool: PgPool, config: Config, mailer: Mailer) -> AppState {
37        let policy = match config.mode {
38            Mode::Cloud => AddressPolicy::Public,
39            Mode::SelfHost => AddressPolicy::AllowPrivate,
40        };
41        AppState {
42            pool,
43            channel_key: ChannelKey::derive(&config.secret_key),
44            notify_http: GuardedHttp::new(policy).expect("http client builds"),
45            config: Arc::new(config),
46            mailer,
47            shutdown: tokio_util::sync::CancellationToken::new(),
48            anon_calls: Arc::new(crate::agent::limiter::CallLimiter::for_anon_tools()),
49            http: reqwest::Client::builder()
50                .timeout(Duration::from_secs(10))
51                .user_agent(concat!("CodoSEO/", env!("CARGO_PKG_VERSION")))
52                .build()
53                .expect("http client builds"),
54        }
55    }
56}
57
58/// A pool that connects on first use with a short acquire timeout, so the web role starts and
59/// serves its 503 page while Postgres is down instead of refusing to boot.
60pub fn lazy_pool(database_url: &str) -> Result<PgPool, sqlx::Error> {
61    PgPoolOptions::new()
62        .max_connections(5)
63        .acquire_timeout(Duration::from_secs(3))
64        .connect_lazy(database_url)
65}