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