diff --git a/.gitignore b/.gitignore index 805bcb7..fd24a46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ server/SHServ/Logs server/SHServ/.env server/Cache/ +server/dist/ Logs/ firmwares/ diff --git a/server/SHServ/Controllers/AppController.php b/server/SHServ/Controllers/AppController.php index 3289976..bba7d3e 100644 --- a/server/SHServ/Controllers/AppController.php +++ b/server/SHServ/Controllers/AppController.php @@ -4,6 +4,12 @@ class AppController extends \SHServ\Middleware\Controller { public function index() { + $indexPath = $this->app()->root_folder() . '/webclient/dist/index.html'; + if (file_exists($indexPath)) { + header('Content-Type: text/html; charset=utf-8'); + readfile($indexPath); + return; + } $server_version = FCONF["version"]; return "Smart home server.
Version {$server_version}"; } diff --git a/server/SHServ/EventsHandlers.php b/server/SHServ/EventsHandlers.php index a9acd6f..2c4334b 100644 --- a/server/SHServ/EventsHandlers.php +++ b/server/SHServ/EventsHandlers.php @@ -12,9 +12,48 @@ }); events() -> handler('kernel:CallControl.no_calls', function(Array $params) { - if(!app() -> console_flag) { - echo "404 not found"; + if(app() -> console_flag) { + return; } + + $uri = $_SERVER['REQUEST_URI'] ?? ''; + if(strpos($uri, '?') !== false) { + list($uri) = explode('?', $uri); + } + + $root = app() -> root_folder() . '/webclient/dist'; + $filePath = $root . $uri; + + if( + (str_starts_with($uri, '/assets/') || $uri === '/favicon.ico') + && file_exists($filePath) + && is_file($filePath) + ) { + $ext = pathinfo($filePath, PATHINFO_EXTENSION); + $mimeTypes = [ + 'js' => 'application/javascript', + 'css' => 'text/css', + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'svg' => 'image/svg+xml', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + 'ttf' => 'font/ttf', + 'eot' => 'application/vnd.ms-fontobject', + 'ico' => 'image/x-icon', + 'json' => 'application/json', + 'map' => 'application/json', + ]; + header('Content-Type: ' . ($mimeTypes[$ext] ?? 'application/octet-stream')); + header('Cache-Control: public, max-age=31536000, immutable'); + readfile($filePath); + return; + } + + http_response_code(404); + echo "404 not found"; }); events() -> handler('app:online', function(Array $params) { diff --git a/webclient/.env b/webclient/.env index a469b08..d6579e4 100644 --- a/webclient/.env +++ b/webclient/.env @@ -1,3 +1,3 @@ VITE_API_BASE_URL= -VITE_API_PROXY_PATH=/proxy.php +VITE_API_PROXY_PATH= VITE_API_TIMEOUT_MS=10000 diff --git a/webclient/config.php b/webclient/config.php deleted file mode 100644 index 188d71a..0000000 --- a/webclient/config.php +++ /dev/null @@ -1,20 +0,0 @@ - "0.4.0 dev", - "server" => "http://smart-home-serv.local", - // "server" => "http://192.168.1.101", - // Какие пути разрешены (белый список) — подстрой под себя - "allowed_prefixes" => [ - "/api/v1/", - "/auth/", - ], - "proxy" => [ - // Кто может обращаться (CORS) - "allowed_origins" => [ - 'http://localhost:5173', - 'http://127.0.0.1:5173', - // 'https://your-frontend-domain.com', - ], - ], -]; diff --git a/webclient/index.php b/webclient/index.php deleted file mode 100644 index adc7e82..0000000 --- a/webclient/index.php +++ /dev/null @@ -1,15 +0,0 @@ -Not built'; - echo '

Client not built

Run npm run build in the webclient directory.

'; - echo ''; -} diff --git a/webclient/proxy.php b/webclient/proxy.php deleted file mode 100644 index 1c65f03..0000000 --- a/webclient/proxy.php +++ /dev/null @@ -1,173 +0,0 @@ - false, - 'message' => $message, - ], $extra), JSON_UNESCAPED_UNICODE); - exit; -} - -function get_request_headers_lower() { - $headers = []; - foreach (getallheaders() as $k => $v) { - $headers[strtolower($k)] = $v; - } - return $headers; -} - -function cors_headers($origin, $allowed_origins) { - if ($origin && in_array($origin, $allowed_origins, true)) { - header("Access-Control-Allow-Origin: {$origin}"); - header("Vary: Origin"); - } else { - // Если хочешь разрешить всем — раскомментируй (но лучше белый список) - header("Access-Control-Allow-Origin: *"); - } - - header("Access-Control-Allow-Credentials: true"); - header("Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS"); - header("Access-Control-Allow-Headers: Authorization, Content-Type, Accept, X-Requested-With"); - header("Access-Control-Max-Age: 86400"); -} - -// ========================= -// CORS preflight -// ========================= -$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; -cors_headers($origin, $allowed_origins); - -if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { - http_response_code(204); - exit; -} - -// ========================= -// Валидация входа -// ========================= - -// Ожидаем ?path=/api/v1/... -$path = $_GET['path'] ?? ''; -if (!$path || $path[0] !== '/') { - send_json_error(400, 'Missing or invalid "path" parameter. Example: ?path=/api/v1/scripts/actions/list'); -} - -// белый список путей -$ok = false; -foreach ($allowed_prefixes as $p) { - if (str_starts_with($path, $p)) { - $ok = true; - break; - } -} -if (!$ok) { - send_json_error(403, 'Path not allowed', ['path' => $path]); -} - -// Собираем URL апстрима + query string (кроме path) -$query = $_GET; -unset($query['path']); -$qs = http_build_query($query); -$upstream_url = rtrim($upstream_base_url, '/') . $path . ($qs ? ('?' . $qs) : ''); - -// ========================= -// Проксирование через cURL -// ========================= -$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; -$headers_in = get_request_headers_lower(); - -$ch = curl_init($upstream_url); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); -curl_setopt($ch, CURLOPT_HEADER, true); // чтобы разделить headers/body -curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); -curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); -curl_setopt($ch, CURLOPT_TIMEOUT, 120); - -// Проксируем body для методов кроме GET/HEAD -$body = file_get_contents('php://input'); -if (!in_array($method, ['GET', 'HEAD'], true)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); -} - -// Заголовки, которые прокидываем -$forward_headers = []; - -// Authorization -if (!empty($headers_in['authorization'])) { - $forward_headers[] = 'Authorization: ' . $headers_in['authorization']; -} - -// Content-Type -if (!empty($headers_in['content-type'])) { - $forward_headers[] = 'Content-Type: ' . $headers_in['content-type']; -} - -// Accept -if (!empty($headers_in['accept'])) { - $forward_headers[] = 'Accept: ' . $headers_in['accept']; -} - -// Cookie — required for session-based auth to work through proxy -if (!empty($headers_in['cookie'])) { - $forward_headers[] = 'Cookie: ' . $headers_in['cookie']; -} - -// Можно добавить свой заголовок, например X-Proxy -$forward_headers[] = 'X-Proxy: php'; - -curl_setopt($ch, CURLOPT_HTTPHEADER, $forward_headers); - -$response = curl_exec($ch); -if ($response === false) { - $err = curl_error($ch); - curl_close($ch); - send_json_error(502, 'Upstream request failed', ['details' => $err]); -} - -$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); -$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); -curl_close($ch); - -$raw_headers = substr($response, 0, $header_size); -$resp_body = substr($response, $header_size); - -// ========================= -// Отдаём ответ клиенту -// ========================= -http_response_code($http_code); - -// Проксируем часть заголовков ответа (без CORS/опасных) -$lines = preg_split("/\r\n|\n|\r/", trim($raw_headers)); -foreach ($lines as $line) { - if (stripos($line, 'HTTP/') === 0) continue; - - $pos = strpos($line, ':'); - if ($pos === false) continue; - - $name = trim(substr($line, 0, $pos)); - $value = trim(substr($line, $pos + 1)); - - $name_l = strtolower($name); - - // не прокидываем hop-by-hop и то, что конфликтует - if (in_array($name_l, ['transfer-encoding', 'content-length', 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'upgrade'], true)) { - continue; - } - - // CORS мы уже выставили сами - if (str_starts_with($name_l, 'access-control-')) { - continue; - } - - header($name . ': ' . $value, false); -} - -echo $resp_body; diff --git a/webclient/src/api/client.js b/webclient/src/api/client.js index f763a3d..5ddd4fe 100644 --- a/webclient/src/api/client.js +++ b/webclient/src/api/client.js @@ -18,7 +18,7 @@ clearAccessToken(); const isLoginPage = window.location.hash.includes("/login"); if (!isLoginPage) { - window.location.href = `/proxy.php?path=/auth/login&return_to=${encodeURIComponent(window.location.href)}`; + window.location.href = `/auth/login?return_to=${encodeURIComponent(window.location.href)}`; } } diff --git a/webclient/src/components/layout/AppShell.vue b/webclient/src/components/layout/AppShell.vue index 054b27b..ee8e97e 100644 --- a/webclient/src/components/layout/AppShell.vue +++ b/webclient/src/components/layout/AppShell.vue @@ -117,7 +117,7 @@ function handleLogin() { const returnTo = window.location.href; - window.location.href = `/proxy.php?path=/auth/login&return_to=${encodeURIComponent(returnTo)}`; + window.location.href = `/auth/login?return_to=${encodeURIComponent(returnTo)}`; } function closeDrawer() { diff --git a/webclient/src/features/auth/pages/LoginPage.vue b/webclient/src/features/auth/pages/LoginPage.vue index 0c9b5d6..30260ae 100644 --- a/webclient/src/features/auth/pages/LoginPage.vue +++ b/webclient/src/features/auth/pages/LoginPage.vue @@ -63,7 +63,7 @@ function handleLogin() { const returnTo = window.location.href; - window.location.href = `/proxy.php?path=/auth/login&return_to=${encodeURIComponent(returnTo)}`; + window.location.href = `/auth/login?return_to=${encodeURIComponent(returnTo)}`; } diff --git a/webclient/src/stores/auth.js b/webclient/src/stores/auth.js index fb4faf5..fee0cd5 100644 --- a/webclient/src/stores/auth.js +++ b/webclient/src/stores/auth.js @@ -68,7 +68,7 @@ function redirectToLogin() { const returnTo = window.location.href; - window.location.href = `/proxy.php?path=/auth/login&return_to=${encodeURIComponent(returnTo)}`; + window.location.href = `/auth/login?return_to=${encodeURIComponent(returnTo)}`; } return { diff --git a/webclient/vite.config.js b/webclient/vite.config.js index f723a6e..6d42a56 100644 --- a/webclient/vite.config.js +++ b/webclient/vite.config.js @@ -1,17 +1,17 @@ import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; -export default defineConfig(({ command }) => ({ - base: command === "build" ? "/dist/" : "/", +export default defineConfig({ + base: "/", plugins: [vue()], + build: { + outDir: "../server/dist", + emptyOutDir: true, + }, server: { host: "0.0.0.0", port: 5173, proxy: { - "/proxy.php": { - target: "http://smart-home-serv.local", - changeOrigin: true, - }, "/api/v1": { target: "http://smart-home-serv.local", changeOrigin: true, @@ -38,4 +38,4 @@ ], }, }, -})); +});