1use 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});