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                .build()
194                .unwrap(),
195            app_router: RwLock::new(None),
196
197            extensions: Arc::new(extensions::manager::ExtensionManager::new(vec![])),
198            updates: Arc::new(updates::UpdateManager::default()),
199            background_tasks: background_tasks.clone(),
200            shutdown_handlers: shutdown_handlers.clone(),
201            settings: settings.clone(),
202            jwt,
203            ntp,
204            storage,
205            captcha,
206            mail,
207            database: database.clone(),
208            cache: cache.clone(),
209            env: env.clone(),
210        });
211
212        Ok(state)
213    }
214
215    pub async fn send_router_oneshot(
216        &self,
217        req: axum::http::Request<axum::body::Body>,
218    ) -> Result<axum::http::Response<axum::body::Body>, anyhow::Error> {
219        let routes_service = self.app_router.read().await;
220        let routes_service = routes_service
221            .as_ref()
222            .ok_or_else(|| anyhow::anyhow!("router not initialized"))?;
223        let routes_service = routes_service.clone();
224
225        let svc = routes_service.oneshot(req);
226        match svc.await {
227            Ok(res) => Ok(res),
228            Err(err) => Err(anyhow::anyhow!(
229                "failed to process request in oneshot router: {:#?}",
230                err
231            )),
232        }
233    }
234
235    pub async fn send_authenticated_router_oneshot(
236        &self,
237        mut req: axum::http::Request<axum::body::Body>,
238        user: models::user::User,
239        auth_method: models::user::AuthMethod,
240    ) -> Result<axum::http::Response<axum::body::Body>, anyhow::Error> {
241        let routes_service = self.app_router.read().await;
242        let routes_service = routes_service
243            .as_ref()
244            .ok_or_else(|| anyhow::anyhow!("router not initialized"))?;
245        let routes_service = routes_service.clone();
246
247        req.extensions_mut().insert((user, auth_method));
248
249        let svc = routes_service.oneshot(req);
250        match svc.await {
251            Ok(res) => Ok(res),
252            Err(err) => Err(anyhow::anyhow!(
253                "failed to process request in oneshot router: {:#?}",
254                err
255            )),
256        }
257    }
258}
259
260pub type State = Arc<AppState>;
261pub type GetState = axum::extract::State<State>;
262
263#[inline(always)]
264#[cold]
265fn cold_path() {}
266
267#[inline(always)]
268pub fn likely(b: bool) -> bool {
269    if b {
270        true
271    } else {
272        cold_path();
273        false
274    }
275}
276
277#[inline(always)]
278pub fn unlikely(b: bool) -> bool {
279    if b {
280        cold_path();
281        true
282    } else {
283        false
284    }
285}
286
287pub const FRONTEND_ASSETS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist");
288
289pub static FRONTEND_LANGUAGES: LazyLock<Vec<compact_str::CompactString>> = LazyLock::new(|| {
290    let mut languages = Vec::new();
291
292    let Some(translations) = FRONTEND_ASSETS.get_dir("translations") else {
293        return languages;
294    };
295
296    for translation in translations.files() {
297        let Some(file_name) = translation.path().file_name() else {
298            continue;
299        };
300
301        languages.push(file_name.to_string_lossy().trim_end_matches(".json").into());
302    }
303
304    languages
305});