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