1use axum::{Router, response::IntoResponse, ServiceExt, handler::HandlerWithoutStateExt};
2use tower_http::services::ServeDir;
3use tower_http::normalize_path::NormalizePathLayer;
4use tower::Layer;
5use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer, key_extractor::SmartIpKeyExtractor};
6use axum_session::{SessionLayer, SessionStore};
7use crate::app::Config;
8use crate::session_manager::RustBasicSessionStore;
9use crate::errors::ErrorController;
10use tower_governor::GovernorError;
11use std::net::SocketAddr;
12use sea_orm::DatabaseConnection;
13use std::sync::Arc;
14use std::process::Command;
15use std::time::Duration;
16use tower_livereload::LiveReloadLayer;
17
18#[derive(Clone)]
19#[allow(dead_code)]
20pub struct AppState {
21 pub db: DatabaseConnection,
22 pub config: Arc<Config>,
23}
24
25static EMBEDDED_PUBLIC_GET: std::sync::OnceLock<fn(&str) -> Option<rust_embed::EmbeddedFile>> = std::sync::OnceLock::new();
26
27pub fn set_embedded_public(f: fn(&str) -> Option<rust_embed::EmbeddedFile>) {
28 EMBEDDED_PUBLIC_GET.set(f).ok();
29}
30
31fn guess_mime(path: &str) -> &'static str {
33 if path.ends_with(".js") {
34 "application/javascript"
35 } else if path.ends_with(".css") {
36 "text/css"
37 } else if path.ends_with(".html") {
38 "text/html"
39 } else if path.ends_with(".png") {
40 "image/png"
41 } else if path.ends_with(".jpg") || path.ends_with(".jpeg") {
42 "image/jpeg"
43 } else if path.ends_with(".svg") {
44 "image/svg+xml"
45 } else if path.ends_with(".ico") {
46 "image/x-icon"
47 } else if path.ends_with(".json") {
48 "application/json"
49 } else if path.ends_with(".woff") {
50 "font/woff"
51 } else if path.ends_with(".woff2") {
52 "font/woff2"
53 } else {
54 "application/octet-stream"
55 }
56}
57
58async fn embedded_fallback_handler(uri: axum::http::Uri) -> impl IntoResponse {
60 let path = uri.path().trim_start_matches('/');
61 let file_path = if path.is_empty() { "index.html" } else { path };
62 let file = EMBEDDED_PUBLIC_GET.get().and_then(|f| f(file_path));
63
64 match file {
65 Some(content) => {
66 let mime = guess_mime(file_path);
67 axum::response::Response::builder()
68 .header(axum::http::header::CONTENT_TYPE, mime)
69 .body(axum::body::Body::from(content.data))
70 .unwrap()
71 }
72 None => ErrorController::not_found().await.into_response(),
73 }
74}
75
76pub async fn start_server(
77 cfg: Config,
78 session_store: SessionStore<RustBasicSessionStore>,
79 db: DatabaseConnection,
80 app_router: Router<AppState>
81) {
82 kill_port_if_in_use(cfg.app_port);
84
85 unsafe {
87 std::env::set_var("TZ", &cfg.app_timezone);
88 }
89
90 let state = AppState {
92 db,
93 config: Arc::new(cfg.clone()),
94 };
95
96 let governor_conf = Arc::new(
98 GovernorConfigBuilder::default()
99 .key_extractor(SmartIpKeyExtractor)
100 .period(Duration::from_millis(1000 / cfg.app_limit_request))
101 .burst_size(cfg.app_limit_request as u32)
102 .finish()
103 .unwrap(),
104 );
105
106 let app = Router::new().merge(app_router);
108
109 let app = if cfg.app_debug {
111 let static_files = ServeDir::new("public");
112 app.fallback_service(static_files.not_found_service(ErrorController::not_found.into_service()))
113 } else {
114 app.fallback(embedded_fallback_handler)
115 };
116
117 let app = app
118 .layer(axum::middleware::from_fn(crate::middleware::security_headers::security_headers_middleware))
119 .layer(axum::middleware::from_fn(crate::middleware::logging::logging_middleware))
120 .layer(GovernorLayer::new(governor_conf))
121 .layer(SessionLayer::new(session_store))
122 .with_state(state);
123
124 let app = if cfg.app_debug {
126 tracing::info!("🔄 Fitur Live Reload (Auto-refresh) diaktifkan.");
127 app.layer(LiveReloadLayer::new())
128 } else {
129 app
130 };
131
132 let app = NormalizePathLayer::trim_trailing_slash().layer(app);
134
135 let addr_str = format!("{}:{}", cfg.app_host, cfg.app_port);
137 let addr: SocketAddr = addr_str.parse().expect("Alamat server tidak valid");
138
139 tracing::info!("{} berjalan di: http://{}", cfg.app_name, addr);
140
141 let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
143 axum::serve(listener, ServiceExt::<axum::extract::Request>::into_make_service_with_connect_info::<SocketAddr>(app)).await.unwrap();
144}
145
146fn kill_port_if_in_use(port: u16) {
148 #[cfg(target_os = "macos")]
149 {
150 let output = Command::new("lsof")
152 .arg("-t")
153 .arg(format!("-i:{}", port))
154 .output();
155
156 if let Ok(out) = output {
157 let pid_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
158 if !pid_str.is_empty() {
159 tracing::warn!("Port {} sedang digunakan oleh PID {}. Membunuh proses...", port, pid_str);
160
161 for pid in pid_str.split('\n') {
163 if !pid.is_empty() {
164 let _ = Command::new("kill")
165 .arg("-9")
166 .arg(pid)
167 .output();
168 }
169 }
170
171 std::thread::sleep(std::time::Duration::from_millis(500));
173 }
174 }
175 }
176
177 #[cfg(target_os = "linux")]
178 {
179 let _ = Command::new("fuser")
180 .arg("-k")
181 .arg(format!("{}/tcp", port))
182 .output();
183 }
184
185 #[cfg(target_os = "windows")]
186 {
187 let output = Command::new("cmd")
188 .args(&["/C", &format!("netstat -ano | findstr :{}", port)])
189 .output();
190
191 if let Ok(out) = output {
192 let stdout = String::from_utf8_lossy(&out.stdout);
193 let mut found = false;
194 for line in stdout.lines() {
195 let parts: Vec<&str> = line.split_whitespace().collect();
196 if let Some(pid) = parts.last() {
197 if pid.parse::<u32>().is_ok() {
198 tracing::warn!("Port {} sedang digunakan oleh PID {}. Membunuh proses...", port, pid);
199 let _ = Command::new("taskkill")
200 .args(&["/F", "/PID", pid])
201 .output();
202 found = true;
203 }
204 }
205 }
206 if found {
207 std::thread::sleep(std::time::Duration::from_millis(500));
209 }
210 }
211 }
212}
213
214#[allow(dead_code)]
216fn handle_governor_error(err: GovernorError) -> axum::response::Response {
217 match err {
218 GovernorError::TooManyRequests { wait_time, .. } => {
219 ErrorController::show(
220 429,
221 &format!("Terlalu banyak permintaan. Silakan tunggu {} detik lagi.", wait_time)
222 ).into_response()
223 },
224 _ => ErrorController::show(500, "Terjadi kesalahan pada sistem pembatas request.").into_response(),
225 }
226}