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 @@
],
},
},
-}));
+});