Skip to main content

shared/
lib.rs

1//! Shared library for the Calagopus Panel.
2//!
3//! This library contains code that is shared between the backend and extensions.
4//! It includes models, utilities, and other common functionality to avoid repetition
5//! and ensure consistency across the project. If something for a job exists in here,
6//! it's generally preferred to be used instead of re-implementing it elsewhere.
7
8use anyhow::Context;
9use colored::Colorize;
10use include_dir::{Dir, include_dir};
11use serde::{Deserialize, Serialize};
12use std::{
13    sync::{Arc, LazyLock},
14    time::Instant,
15};
16use tokio::sync::RwLock;
17use tower::util::ServiceExt;
18use utoipa::ToSchema;
19
20pub mod cache;
21pub mod cap;
22pub mod captcha;
23pub mod database;
24pub mod deserialize;
25pub mod env;
26pub mod events;
27pub mod extensions;
28pub mod extract;
29pub mod git;
30#[cfg(unix)]
31pub mod heavy;
32pub mod jwt;
33pub mod mail;
34pub mod models;
35pub mod net;
36pub mod ntp;
37pub mod payload;
38pub mod permissions;
39pub mod prelude;
40pub mod response;
41pub mod settings;
42pub mod storage;
43pub mod telemetry;
44pub mod updates;
45pub mod utils;
46
47pub use payload::Payload;
48pub use schema_extension_core::Extendible;
49
50pub const VERSION: &str = env!("CARGO_PKG_VERSION");
51pub const GIT_COMMIT: &str = env!("CARGO_GIT_COMMIT");
52pub const GIT_BRANCH: &str = env!("CARGO_GIT_BRANCH");
53pub const TARGET: &str = env!("CARGO_TARGET");
54
55pub fn full_version() -> String {
56    if GIT_BRANCH == "unknown" {
57        VERSION.to_string()
58    } else {
59        format!("{VERSION}:{GIT_COMMIT}@{GIT_BRANCH}")
60    }
61}
62
63pub const BUFFER_SIZE: usize = 32 * 1024;
64
65pub type GetIp = axum::extract::Extension<std::net::IpAddr>;
66
67#[derive(ToSchema, Serialize)]
68pub struct ApiError {
69    pub errors: Vec<String>,
70}
71
72impl ApiError {
73    #[inline]
74    pub fn new_value(errors: &[&str]) -> serde_json::Value {
75        serde_json::json!({
76            "errors": errors,
77        })
78    }
79
80    #[inline]
81    pub fn new_strings_value(errors: Vec<String>) -> serde_json::Value {
82        serde_json::json!({
83            "errors": errors,
84        })
85    }
86
87    #[inline]
88    pub fn new_wings_value(error: wings_api::ApiError) -> serde_json::Value {
89        serde_json::json!({
90            "errors": [error.error],
91        })
92    }
93
94    #[inline]
95    pub fn new_database_agent_value(error: db_agent_api::ApiError) -> serde_json::Value {
96        serde_json::json!({
97            "errors": [error.error],
98        })
99    }
100}
101
102#[derive(Debug, ToSchema, Deserialize, Serialize, Clone, Copy)]
103#[serde(rename_all = "snake_case")]
104pub enum AppContainerType {
105    Official,
106    OfficialAIO,
107    OfficialHeavy,
108    OfficialHeavyAIO,
109    Unknown,
110    None,
111}
112
113impl AppContainerType {
114    pub fn detect() -> Self {
115        match std::env::var("OCI_CONTAINER").as_deref() {
116            Ok("official") => AppContainerType::Official,
117            Ok("official-aio") => AppContainerType::OfficialAIO,
118            Ok("official-heavy") => AppContainerType::OfficialHeavy,
119            Ok("official-heavy-aio") => AppContainerType::OfficialHeavyAIO,
120            Ok(_) => AppContainerType::Unknown,
121            Err(_) => AppContainerType::None,
122        }
123    }
124
125    #[inline]
126    pub fn is_all_in_one(&self) -> bool {
127        matches!(
128            self,
129            AppContainerType::OfficialAIO | AppContainerType::OfficialHeavyAIO
130        )
131    }
132
133    #[inline]
134    pub fn is_heavy(&self) -> bool {
135        matches!(
136            self,
137            AppContainerType::OfficialHeavy | AppContainerType::OfficialHeavyAIO
138        )
139    }
140}
141
142pub struct AppState {
143    pub start_time: Instant,
144    pub container_type: AppContainerType,
145    pub version: String,
146
147    pub client: reqwest::Client,
148    pub app_router: RwLock<Option<axum::Router>>,
149
150    pub extensions: Arc<extensions::manager::ExtensionManager>,
151    pub updates: Arc<updates::UpdateManager>,
152    pub background_tasks: Arc<extensions::background_tasks::BackgroundTaskManager>,
153    pub shutdown_handlers: Arc<extensions::shutdown_handlers::ShutdownHandlerManager>,
154    pub settings: Arc<settings::Settings>,
155    pub jwt: Arc<jwt::Jwt>,
156    pub ntp: Arc<ntp::Ntp>,
157    pub storage: Arc<storage::Storage>,
158    pub captcha: Arc<captcha::Captcha>,
159    pub mail: Arc<mail::Mail>,
160    pub database: Arc<database::Database>,
161    pub cache: Arc<cache::Cache>,
162    pub env: Arc<env::Env>,
163}
164
165impl AppState {
166    pub async fn new_cli(env: Option<Arc<env::Env>>) -> Result<State, anyhow::Error> {
167        let env = match env {
168            Some(env) => env,
169            None => {
170                eprintln!(
171                    "{}",
172                    "please setup the new panel environment before using this command.".red()
173                );
174                std::process::exit(1);
175            }
176        };
177
178        let jwt = Arc::new(jwt::Jwt::new(&env));
179        let ntp = ntp::Ntp::new();
180        let cache = cache::Cache::new(&env).await;
181        let database = Arc::new(database::Database::new(&env, cache.clone()).await);
182
183        let background_tasks =
184            Arc::new(extensions::background_tasks::BackgroundTaskManager::default());
185        let shutdown_handlers =
186            Arc::new(extensions::shutdown_handlers::ShutdownHandlerManager::default());
187        let settings = Arc::new(
188            settings::Settings::new(database.clone())
189                .await
190                .context("failed to load settings")?,
191        );
192        let storage = Arc::new(storage::Storage::new(settings.clone()));
193        let captcha = Arc::new(captcha::Captcha::new(settings.clone()));
194        let mail = Arc::new(mail::Mail::new(settings.clone()));
195
196        let state = Arc::new(AppState {
197            start_time: Instant::now(),
198            container_type: AppContainerType::detect(),
199            version: full_version(),
200
201            client: reqwest::ClientBuilder::new()
202                .user_agent(format!("github.com/calagopus/panel {}", VERSION))
203                .connect_timeout(std::time::Duration::from_secs(10))
204                .build()
205                .unwrap(),
206            app_router: RwLock::new(None),
207
208            extensions: Arc::new(extensions::manager::ExtensionManager::new(vec![])),
209            updates: Arc::new(updates::UpdateManager::default()),
210            background_tasks: background_tasks.clone(),
211            shutdown_handlers: shutdown_handlers.clone(),
212            settings: settings.clone(),
213            jwt,
214            ntp,
215            storage,
216            captcha,
217            mail,
218            database: database.clone(),
219            cache: cache.clone(),
220            env: env.clone(),
221        });
222
223        Ok(state)
224    }
225
226    pub async fn send_router_oneshot(
227        &self,
228        req: axum::http::Request<axum::body::Body>,
229    ) -> Result<axum::http::Response<axum::body::Body>, anyhow::Error> {
230        let routes_service = self.app_router.read().await;
231        let routes_service = routes_service
232            .as_ref()
233            .ok_or_else(|| anyhow::anyhow!("router not initialized"))?;
234        let routes_service = routes_service.clone();
235
236        let svc = routes_service.oneshot(req);
237        match svc.await {
238            Ok(res) => Ok(res),
239            Err(err) => Err(anyhow::anyhow!(
240                "failed to process request in oneshot router: {:#?}",
241                err
242            )),
243        }
244    }
245
246    pub async fn send_authenticated_router_oneshot(
247        &self,
248        mut req: axum::http::Request<axum::body::Body>,
249        user: models::user::User,
250        auth_method: models::user::AuthMethod,
251    ) -> Result<axum::http::Response<axum::body::Body>, anyhow::Error> {
252        let routes_service = self.app_router.read().await;
253        let routes_service = routes_service
254            .as_ref()
255            .ok_or_else(|| anyhow::anyhow!("router not initialized"))?;
256        let routes_service = routes_service.clone();
257
258        req.extensions_mut().insert((user, auth_method));
259
260        let svc = routes_service.oneshot(req);
261        match svc.await {
262            Ok(res) => Ok(res),
263            Err(err) => Err(anyhow::anyhow!(
264                "failed to process request in oneshot router: {:#?}",
265                err
266            )),
267        }
268    }
269}
270
271pub type State = Arc<AppState>;
272pub type GetState = axum::extract::State<State>;
273
274#[inline(always)]
275#[cold]
276fn cold_path() {}
277
278#[inline(always)]
279pub fn likely(b: bool) -> bool {
280    if b {
281        true
282    } else {
283        cold_path();
284        false
285    }
286}
287
288#[inline(always)]
289pub fn unlikely(b: bool) -> bool {
290    if b {
291        cold_path();
292        true
293    } else {
294        false
295    }
296}
297
298pub const FRONTEND_ASSETS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist");
299
300pub static FRONTEND_LANGUAGES: LazyLock<Vec<compact_str::CompactString>> = LazyLock::new(|| {
301    let mut languages = Vec::new();
302
303    let Some(translations) = FRONTEND_ASSETS.get_dir("translations") else {
304        return languages;
305    };
306
307    for translation in translations.files() {
308        let Some(file_name) = translation.path().file_name() else {
309            continue;
310        };
311        let file_name = file_name.to_string_lossy();
312        let Some(lang) = file_name.strip_suffix(".json") else {
313            continue;
314        };
315
316        languages.push(lang.into());
317    }
318
319    languages
320});