diff --git a/.DS_Store b/.DS_Store index 20ebb06..dd8bcdf 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/backend/app/.DS_Store b/backend/app/.DS_Store deleted file mode 100644 index 175ed52..0000000 Binary files a/backend/app/.DS_Store and /dev/null differ diff --git a/backend/app/.env b/backend/app/.env index 249b96d..0609530 100644 --- a/backend/app/.env +++ b/backend/app/.env @@ -4,3 +4,5 @@ LXD_API_URL=https://localhost:8443 LXD_CLIENT_CERT=/etc/ssl/lxdapp/client.crt LXD_CLIENT_KEY=/etc/ssl/lxdapp/client.key LXD_IMAGE_FINGERPRINT=2edfd84b1396 +AUTH_KEY=R9kX2HFA7ZjLdVYm8TsQWpCeNuB1v0GrS6MI4axf + diff --git a/backend/app/README.md b/backend/app/README.md index 9edf7a0..ea4d376 100644 --- a/backend/app/README.md +++ b/backend/app/README.md @@ -61,27 +61,8 @@ backend/ 3. **Ensure PHP and NGINX are configured** -NGINX config example: - ```bash - server { - listen 80; - server_name *.lxdapp.local; + Check in backend/nginx.conf - root /var/www/html/lxd-app/backend/public; - index index.php; - - location / { - try_files $uri /index.php?$query_string; - } - - location ~ \.php$ { - fastcgi_pass unix:/run/php/php8.4-fpm.sock; - fastcgi_index index.php; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_read_timeout 300; - } - } 4. **Map domain in /etc/hosts** diff --git a/backend/app/composer.json b/backend/app/composer.json index 52def26..490f494 100644 --- a/backend/app/composer.json +++ b/backend/app/composer.json @@ -3,9 +3,8 @@ "slim/slim": "4.*", "slim/psr7": "^1.7", "php-di/php-di": "^7.0", - "guzzlehttp/guzzle": "^7.9", - "nyholm/psr7": "^1.8", - "vlucas/phpdotenv": "^5.6" + "vlucas/phpdotenv": "^5.6", + "zounar/php-proxy": "^1.1" }, "autoload": { "psr-4": { diff --git a/backend/app/config.json b/backend/app/config.json index 1961c59..623df91 100644 --- a/backend/app/config.json +++ b/backend/app/config.json @@ -1,3 +1,3 @@ { - "test.lxdapp.local": "container-test" + "testone.lxdapp.local": "testone" } \ No newline at end of file diff --git a/backend/app/nginx.conf b/backend/app/nginx.conf new file mode 100644 index 0000000..9552d7a --- /dev/null +++ b/backend/app/nginx.conf @@ -0,0 +1,45 @@ +server { + listen 80; + server_name *.lxdapp.local lxdapp.local; + + root /var/www/html/lxd-app/backend/app/public; # Adjust this to your Slim project's public folder + index index.php index.html index.htm; + + # Reverse proxy to Vue.js for /app/ route + location ^~ /app/ { + proxy_pass http://127.0.0.1:3000/app/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + # rewrite ^/app/(/.*)$ $1 break; + } + + # (Optional: Serve static assets directly if needed) + # location /app/_nuxt/ { + # proxy_pass http://127.0.0.1:3000; + # } + + # Handle PHP Slim + location / { + try_files $uri $uri/ /index.php?$query_string; + } + # Pass PHP scripts to PHP-FPM + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/php8.4-fpm.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + + # Static file optimization (optional) + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf)$ { + expires 30d; + access_log off; + } + + error_log /var/log/nginx/tutorial_error.log; + access_log /var/log/nginx/tutorial_access.log; +} diff --git a/backend/app/public/index.php b/backend/app/public/index.php index 41f3055..b66272f 100644 --- a/backend/app/public/index.php +++ b/backend/app/public/index.php @@ -5,6 +5,12 @@ declare(strict_types=1); use DI\ContainerBuilder; use Slim\Factory\AppFactory; use Dotenv\Dotenv; +use App\Middleware\CorsMiddleware; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use App\Controllers\CaptchaController; +use App\Controllers\ProxyController; +use App\Controllers\LoginController; require __DIR__ . '/../vendor/autoload.php'; @@ -32,11 +38,11 @@ $containerBuilder->useAutowiring(true); // Enable autowiring globally // Add settings -$settings = require __DIR__ . '/../src/Settings/settings.php'; +$settings = require __DIR__ . '/../src/Settings/Settings.php'; $settings($containerBuilder); // Add dependencies -$dependencies = require __DIR__ . '/../src/Dependencies/dependencies.php'; +$dependencies = require __DIR__ . '/../src/Dependencies/Dependencies.php'; $dependencies($containerBuilder); // Build the container @@ -49,22 +55,23 @@ AppFactory::setContainer($container); $app = AppFactory::create(); // ๐Ÿ”น CORS middleware -$app->add(function ($request, $handler) { - $response = $handler->handle($request); - $origin = $_SERVER['HTTP_ORIGIN'] ?? '*'; +$app->add(CorsMiddleware::class); - return $response - ->withHeader('Access-Control-Allow-Origin', $origin) - ->withHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization') - ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS') - ->withHeader('Access-Control-Allow-Credentials', 'true'); -}); // Register middleware -(require __DIR__ . '/../src/Bootstrap/middleware.php')($app); +(require __DIR__ . '/../src/Bootstrap/Middleware.php')($app); + // Register routes -(require __DIR__ . '/../src/Bootstrap/routes.php')($app); + +// API contianer proxy route +$app->group('/api', function ($group) { + $group->get('/captcha', [CaptchaController::class, 'get']); + $group->post('/login', [LoginController::class, 'index']); + +}); + +$app->any('/{routes:.*}', [ProxyController::class, 'forward']); // Run app $app->run(); diff --git a/backend/app/public/last-access-logs/.DS_Store b/backend/app/public/last-access-logs/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/backend/app/public/last-access-logs/.DS_Store and /dev/null differ diff --git a/backend/app/public/last-access-logs/testone.txt b/backend/app/public/last-access-logs/testone.txt new file mode 100644 index 0000000..0c27550 --- /dev/null +++ b/backend/app/public/last-access-logs/testone.txt @@ -0,0 +1,27 @@ +2025-07-15 18:59:58 : http://lxdapp.local/api/login +2025-07-15 18:59:59 : http://10.110.90.24//demo +2025-07-15 19:00:26 : http://10.110.90.24// +2025-07-15 19:01:46 : http://lxdapp.local/api/login +2025-07-15 19:01:47 : http://10.110.90.24// +2025-07-15 19:02:21 : http://10.110.90.24// +2025-07-15 19:04:09 : http://lxdapp.local/api/login +2025-07-15 19:04:10 : http://10.110.90.24// +2025-07-15 19:11:18 : http://lxdapp.local/api/login +2025-07-15 19:11:18 : http://10.110.90.24// +2025-07-15 19:12:44 : http://10.110.90.24//sdfsd +2025-07-15 19:13:12 : http://10.110.90.24// +2025-07-15 19:17:17 : http://10.110.90.24// +2025-07-15 19:25:26 : http://testone.lxdapp.local/api/login +2025-07-15 19:25:28 : http://10.110.90.24// +2025-07-16 09:15:04 : http://10.110.90.24// +2025-07-16 09:21:18 : http://10.110.90.24// +2025-07-16 09:22:08 : http://testone.lxdapp.local/api/login +2025-07-16 09:22:09 : http://10.110.90.24// +2025-07-16 09:30:13 : http://10.110.90.24// +2025-07-16 09:32:41 : http://testone.lxdapp.local/api/login +2025-07-16 09:32:42 : http://10.110.90.24// +2025-07-16 09:33:50 : http://testone.lxdapp.local/api/login +2025-07-16 09:33:50 : http://10.110.90.24// +2025-07-16 09:33:58 : http://10.110.90.24// +2025-07-16 09:45:10 : http://10.110.90.24// +2025-07-16 09:45:12 : http://10.110.90.24// diff --git a/backend/app/src/Bootstrap/routes.php b/backend/app/src/Bootstrap/routes.php deleted file mode 100644 index b6c24a5..0000000 --- a/backend/app/src/Bootstrap/routes.php +++ /dev/null @@ -1,11 +0,0 @@ -getHeaderLine('Origin'); + if (!empty($origin)) { + $domain = parse_url($origin, PHP_URL_HOST); + } else { + $domain = $request->getHeaderLine('Host'); + } + + $configPath = __DIR__ . '/../../config.json'; + $config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : []; + $name = $config[$domain] ?? null; + + $params = (array)$request->getParsedBody(); + + $captcha = new PCaptcha(); + + if (!$captcha->validate_captcha($params['panswer'])) { + return $this->json($response, ['status' => 'error', 'message' => 'Invalid CAPTCHA'], 200); + } + + $lxd = new LxdService(); + + $status = $lxd->getContainerState($name)['metadata']['status'] ?? 'Stopped'; + if ($status !== 'Running') { + $lxd->startContainer($name); + sleep(10); + + } + + // Write log + LogWriterHelper::write($name, $request->getUri()); + + // Login success + return $this->json($response, ['status' => 'success', 'message' => 'Container started!']); + } + + /** + * Sends a JSON response. + */ + protected function json($response, array $data, int $status = 200) + { + $payload = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + $response->getBody()->write($payload); + + return $response + ->withHeader('Content-Type', 'application/json') + ->withHeader('Access-Control-Allow-Origin', '*') + ->withStatus($status); + } + +} diff --git a/backend/app/src/Controllers/ProxyController.php b/backend/app/src/Controllers/ProxyController.php index 50248b2..f94153a 100644 --- a/backend/app/src/Controllers/ProxyController.php +++ b/backend/app/src/Controllers/ProxyController.php @@ -4,10 +4,10 @@ namespace App\Controllers; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; -use GuzzleHttp\Client; -use App\Utils\SubdomainHelper; use App\Services\LxdService; -use App\lib\PCaptcha; +use App\Lib\PCaptcha; +use Zounar\PHPProxy\Proxy; +use App\Utils\LogWriterHelper; class ProxyController { @@ -16,102 +16,62 @@ class ProxyController */ public function forward(Request $request, Response $response): Response { - $mainDomain = $_ENV['MAIN_DOMAIN'] ?? 'lxdapp.local'; + try { + $mainDomain = $_ENV['MAIN_DOMAIN'] ?? 'lxdapp.local'; - $origin = $request->getHeaderLine('Origin'); - $domain = parse_url($origin, PHP_URL_HOST); // e.g. customer.lxdapp.local - $params = (array)$request->getParsedBody(); + $origin = $request->getHeaderLine('Origin'); + if (!empty($origin)) { + $domain = parse_url($origin, PHP_URL_HOST); + } else { + $domain = $request->getHeaderLine('Host'); + } - $configPath = __DIR__ . '/../../config.json'; - $config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : []; - $name = $config[$domain] ?? null; - - $lxd = new LxdService(); - // print_r($params); - // CASE 1: From Login Page โ€“ Create if not mapped - if (isset($params['source']) && $params['source'] === 'login') { + $configPath = __DIR__ . '/../../config.json'; + $config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : []; + $name = $config[$domain] ?? null; - $captcha = new PCaptcha(); - - if (!$captcha->validate_captcha($params['panswer'])) { - return $this->json($response, ['status' => 'error', 'error' => 'invalid_captcha']); - } + $lxd = new LxdService(); + // STEP 1: If container mapping not found if (!$name) { - $subdomain = SubdomainHelper::getSubdomain($domain, $mainDomain); - $name = $this->generateContainerName($subdomain); // or just use subdomain - $config[$domain] = $name; - file_put_contents($configPath, json_encode($config, JSON_PRETTY_PRINT)); + return $this->json($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404); } + // STEP 2: Check if container exists in LXD if (!$lxd->containerExists($name)) { - $lxd->createContainerAndWait($name); - sleep(5); - //$lxd->startContainer($name); - $lxd->installPackages($name); - sleep(5); + return $this->json($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404); } + // STEP 4: Check container status + $containerInfo = $lxd->getContainerState($name); + $status = $containerInfo['metadata']['status'] ?? 'Stopped'; + + if ($status !== 'Running') { + // Container not running โ†’ redirect to login page + $scheme = $request->getUri()->getScheme(); // "http" or "https" + $redirectUrl = $request->getUri(); + + return $response + ->withHeader('Location', 'app/?auth=ok&redirect=' . urlencode($redirectUrl)) + ->withStatus(302); + } + + // STEP 5: Container is running โ†’ proxy request $ip = $lxd->getContainerIP($name); - if (!$ip) { - return $this->json($response, [ - 'status' => 'error', - 'message' => "Failed to get container IP for '$name'" - ], 500); + return $this->json($response, ['status' => 'error', 'message' => 'Could not fetch container IP'], 500); } - // if (!$lxd->waitForPort($ip, 80, 30)) { - // return $this->json($response, ['status' => 'error', 'message' => 'Container not ready'], 500); - // } - - return $this->json($response, ['status' => 'success', 'ip' => $ip]); + return $this->proxyToContainer($request, $response, $ip, $name); + } catch (\Throwable $e) { + // Global fallback for any exception + return $this->json($response, [ + 'status' => 'error', + 'message' => 'Internal Server Error: ' . $e->getMessage() + ], 500); } - - // CASE 2: Not from login and no mapping - if (!$name) { - return $this->json($response, ['status' => 'not-found']); - } - - // CASE 3: Check if container exists in LXD - $containerInfo = $lxd->getContainerState($name); - if (!$containerInfo) { - return $this->json($response, ['status' => 'not-found']); - } - - if ($containerInfo['metadata']['status'] !== 'Running') { - $lxd->startContainer($name); - sleep(5); - } - - - $ip = $lxd->getContainerIP($name); - // if (!$ip) { - // $ip = $lxd->getContainerIP($name); - // } - - if (!$lxd->waitForPort($ip, 80, 30)) { - return $this->json($response, ['status' => 'error', 'message' => 'Service not available'], 500); - } - - return $this->proxyToContainer($request, $response, $ip, $name); } - /** - * Maps a domain to a container name. - */ - private function mapDomainToContainer(string $domain): ?string - { - $configPath = __DIR__ . '/../../config.json'; - - if (!file_exists($configPath)) { - return null; - } - - $config = json_decode(file_get_contents($configPath), true); - - return $config[$domain] ?? null; - } /** * Sends a JSON response. @@ -128,130 +88,43 @@ class ProxyController ->withStatus($status); } - /** - * Generates a sanitized container name based on a subdomain. - */ - public function generateContainerName(string $subdomain): string - { - // Convert to lowercase, remove unsafe characters - $sanitized = preg_replace('/[^a-z0-9\-]/', '-', strtolower($subdomain)); - - // Optionally, ensure it's prefixed/suffixed for uniqueness - return "container-{$sanitized}"; - } - /** * Proxies the request to the container. */ private function proxyToContainer(Request $request, Response $response, string $ip, string $name): Response { - $client = new Client([ - 'http_errors' => false, - 'timeout' => 10, - ]); - - $method = $request->getMethod(); - - $baseUrl = $ip ? "http://$ip" : "http://127.0.0.1:3000"; + $baseUrl = $ip ? "http://$ip/" : "http://127.0.0.1:3000"; + $uri = $request->getUri(); $path = $uri->getPath(); - + // Remove /api/v1 prefix from path - $prefix = '/api/v1'; + $prefix = '/api/'; if (strpos($path, $prefix) === 0) { $path = substr($path, strlen($prefix)); if ($path === '') { $path = '/'; } } - - // Add .php extension if not present - if (!str_ends_with($path, '.php')) { - $path .= '.php'; - } - + $query = $uri->getQuery(); + $targetUrl = $baseUrl . $path . ($query ? '?' . $query : ''); - $options = [ - 'headers' => [], - 'http_errors' => false, - 'debug' => false, - ]; - foreach ($request->getHeaders() as $headerName => $values) { - if (strtolower($headerName) !== 'host') { - $options['headers'][$headerName] = implode(', ', $values); - } - } + Proxy::$AUTH_KEY = $_ENV['AUTH_KEY'] ?? 'Bj5pnZEX6DkcG6Nz6AjDUT1bvcGRVhRaXDuKDX9CjsEs2'; + Proxy::$ENABLE_AUTH = true; // Enable auth + Proxy::$HEADER_HTTP_PROXY_AUTH = 'HTTP_PROXY_AUTH'; // Ensure it matches the key - $options['headers'] = [ - 'Host' => (string) $ip, - 'Accept-Encoding' => 'gzip, deflate', - 'Accept' => '*/*', - 'User-Agent' => 'SlimProxy/1.0', - ]; + $_SERVER['HTTP_PROXY_AUTH'] = $_ENV['AUTH_KEY'] ?? 'Bj5pnZEX6DkcG6Nz6AjDUT1bvcGRVhRaXDuKDX9CjsEs2'; + $_SERVER['HTTP_PROXY_TARGET_URL'] = $targetUrl; + // Do your custom logic before running proxy + $responseCode = Proxy::run(); - if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) { - $body = (string) $request->getBody(); - if ($body) { - $options['body'] = $body; - } - } - $forwarded = $client->request($method, $targetUrl, $options); + // Write log + LogWriterHelper::write($name, $targetUrl); - $this->writeLastAccessLog($name, $targetUrl); - - foreach ($forwarded->getHeaders() as $headerName => $headerValues) { - $response = $response->withHeader($headerName, implode(', ', $headerValues)); - } - - $response->getBody()->write((string) $forwarded->getBody()); - - return $response->withStatus($forwarded->getStatusCode()); + return $response; } - private function proxyToContainerOLD(Request $request, Response $response, string $ip, string $name): Response - { - $target = $ip ? "http://$ip:80" : "http://127.0.0.1:3000"; - - $client = new Client([ - 'http_errors' => false, - 'timeout' => 5, - ]); - - // Just make a GET request to the base URL - $forwarded = $client->request('GET', $target); - - $this->writeLastAccessLog($name, $target); - - // Return the body and status - $response->getBody()->write((string) $forwarded->getBody()); - return $response - ->withHeader('Content-Type', 'text/html') // or json if API - ->withHeader('Access-Control-Allow-Origin', '*') - ->withStatus($forwarded->getStatusCode()); - - } - - /** - * Logs the last access to a container. - */ - protected function writeLastAccessLog(string $name, string $uri): void { - // Dynamically resolve the log directory relative to the current file - $logDir = realpath(__DIR__ . '/../../public/last-access-logs'); - - // If the resolved path doesn't exist (e.g., public dir was missing), create it - if (!$logDir) { - $logDir = __DIR__ . '/../../public/last-access-logs'; - if (!file_exists($logDir)) { - mkdir($logDir, 0777, true); - } - } - - $logLine = date("Y-m-d H:i:s") . " : " . $uri . "\n"; - file_put_contents($logDir . '/' . $name . '.txt', $logLine, FILE_APPEND); - } - - } \ No newline at end of file diff --git a/backend/app/src/Middleware/CorsMiddleware.php b/backend/app/src/Middleware/CorsMiddleware.php new file mode 100644 index 0000000..06140fd --- /dev/null +++ b/backend/app/src/Middleware/CorsMiddleware.php @@ -0,0 +1,28 @@ +getMethod() === 'OPTIONS') { + $response = new Response(204); + } else { + $response = $handler->handle($request); + } + + return $response + ->withHeader('Access-Control-Allow-Origin', $origin) + ->withHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization') + ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS') + ->withHeader('Access-Control-Allow-Credentials', 'true'); + } +} diff --git a/backend/app/src/Routes/v1/proxy.php b/backend/app/src/Routes/v1/proxy.php deleted file mode 100644 index 5e24fdd..0000000 --- a/backend/app/src/Routes/v1/proxy.php +++ /dev/null @@ -1,11 +0,0 @@ -group('/api/v1', function ($group) { - $group->any('/{routes:.*}', [ProxyController::class, 'forward']); - }); -}; diff --git a/backend/app/src/Routes/web.php b/backend/app/src/Routes/web.php deleted file mode 100644 index 5946310..0000000 --- a/backend/app/src/Routes/web.php +++ /dev/null @@ -1,19 +0,0 @@ -get('/', function (ServerRequestInterface $request, ResponseInterface $response) { - $response->getBody()->write('Welcome to the LXD App!'); - return $response; - }); - - // Captcha route - $app->get('/api/captcha', [CaptchaController::class, 'get']); - -}; diff --git a/backend/app/src/Services/LxdService.php b/backend/app/src/Services/LxdService.php index 792c807..cfa1e5b 100644 --- a/backend/app/src/Services/LxdService.php +++ b/backend/app/src/Services/LxdService.php @@ -6,11 +6,9 @@ use Exception; class LxdService { private string $baseUrl; - private string $imageFingerprint; public function __construct() { $this->baseUrl = $_ENV['LXD_API_URL'] ?? 'https://localhost:8443'; - $this->imageFingerprint = $_ENV['LXD_IMAGE_FINGERPRINT'] ?? '2edfd84b1396'; } @@ -70,20 +68,6 @@ class LxdService return $json; } - /** - * Retrieves a container. - * - * @param string $name Container name - * @return array|null Container or null if an error occurs - */ - public function getContainer(string $name) { - try { - return $this->request('GET', "/1.0/instances/$name"); - } catch (\Throwable $e) { - return null; - } - } - /** * Retrieves the status of a container. * @@ -139,9 +123,7 @@ class LxdService throw new \Exception("Failed to start container: " . json_encode($startOpResp)); } - // 6. Return final container info - $containerResponse = $this->getContainer($name); - return $containerResponse['metadata'] ?? []; + return $startResponse; } /** @@ -159,111 +141,6 @@ class LxdService return $response; } - - /** - * Creates a new container. - * - * @param string $name Container name - * @param string $fingerprint Image fingerprint - * @return array Response from the API - */ - public function createContainer(string $name): array { - $response = $this->request('POST', "/1.0/instances", [ - "name" => $name, - "source" => [ - "type" => "image", - "fingerprint" => $this->imageFingerprint - ] - ]); - sleep(5); - return $response; - } - - /** - * Installs packages inside a container. - * - * @param string $name Container name - * @throws Exception - */ - public function installPackages(string $name) { - $log = ''; - - $log .= "=== apt update ===\n"; - $out1 = shell_exec("/snap/bin/lxc exec $name -- bash -c 'apt update 2>&1'"); - $log .= $out1 . "\n\n"; - - $log .= "=== apt install ===\n"; - $out2 = shell_exec("/snap/bin/lxc exec $name -- bash -c 'apt install -y nginx mysql-server 2>&1'"); - $log .= $out2 . "\n\n"; - - file_put_contents('/tmp/lxd_install.log', $log); - - // Wait for services to start - $this->waitForService($name, 'nginx'); - $this->waitForService($name, 'mysql'); - } - - /** - * Waits for a service to become active inside a container. - * - * @param string $name Container name - * @param string $service Service name - * @throws Exception - */ - private function waitForService(string $name, string $service) - { - for ($i = 0; $i < 10; $i++) { - $status = shell_exec("/snap/bin/lxc exec $name -- systemctl is-active $service"); - if (trim($status) === 'active') { - return; - } - sleep(2); - } - - throw new Exception("Failed to start $service inside container $name"); - } - - /** - * Creates a new container and wait for start. - * - * @param string $name Container name - * @param string $fingerprint Image fingerprint - * @return array Response from the API - */ - public function createContainerAndWait(string $name, array $config = []): array { - $body = [ - "name" => $name, - "source" => array_merge([ - "type" => "image", - "fingerprint" => $this->imageFingerprint - ]), - ]; - - $body = array_merge($body, $config); - - // 2. Send creation request - $response = $this->request('POST', '/1.0/instances', $body); - - if (!isset($response['operation'])) { - throw new \Exception("No operation returned from create request"); - } - - $operationUrl = $response['operation']; - - // 3. Wait for container creation to finish - do { - sleep(1); - $opResponse = $this->request('GET', $operationUrl); - $statusCode = $opResponse['metadata']['status_code'] ?? 0; - } while ($statusCode < 200); - - if ($statusCode >= 400) { - throw new \Exception("Container creation failed: " . json_encode($opResponse)); - } - - // 4. Start the container - return $this->startContainer($name); - } /** @@ -278,6 +155,27 @@ class LxdService return $this->getIPv4FromMetadata($container['metadata']); } + /** + * Extracts the IPv4 address from container metadata. + * + * @param array $metadata Container metadata + * @return string|null IPv4 address or null if not found + */ + public function getIPv4FromMetadata(array $metadata): ?string + { + if ( + isset($metadata['network']['eth0']['addresses']) && + is_array($metadata['network']['eth0']['addresses']) + ) { + foreach ($metadata['network']['eth0']['addresses'] as $addr) { + if (isset($addr['family']) && $addr['family'] === 'inet' && isset($addr['address'])) { + return $addr['address']; + } + } + } + return null; + } + /** * Waits for a specific port to become available. * @@ -303,26 +201,4 @@ class LxdService return false; // Timed out } - - - /** - * Extracts the IPv4 address from container metadata. - * - * @param array $metadata Container metadata - * @return string|null IPv4 address or null if not found - */ - public function getIPv4FromMetadata(array $metadata): ?string - { - if ( - isset($metadata['network']['eth0']['addresses']) && - is_array($metadata['network']['eth0']['addresses']) - ) { - foreach ($metadata['network']['eth0']['addresses'] as $addr) { - if (isset($addr['family']) && $addr['family'] === 'inet' && isset($addr['address'])) { - return $addr['address']; - } - } - } - return null; - } } \ No newline at end of file diff --git a/backend/app/src/Utils/LogWriterHelper.php b/backend/app/src/Utils/LogWriterHelper.php new file mode 100644 index 0000000..74d461f --- /dev/null +++ b/backend/app/src/Utils/LogWriterHelper.php @@ -0,0 +1,21 @@ +getQueryParams()['source'] ?? null; - $domain = $request->getUri()->getHost(); // e.g. customer1.lxdapp.local - - $name = $this->mapDomainToContainer($domain); // Use your config mapping - $ip = $this->getContainerIP($name); - - if ($source === 'login') { - // Trigger provisioning if requested from login - if (!$this->containerExists($name)) { - $this->createContainer($name); - $this->startContainer($name); - $this->installPackages($name); - - // Wait for container to be ready - if (!$this->waitForPort($ip, 80)) { - return $this->json($response, ['status' => 'error', 'message' => 'Container not ready'], 500); - } - } - - return $this->json($response, ['status' => 'success', 'ip' => $ip]); - } - - // For non-login (main page access) - if (!$this->containerExists($name)) { - return $this->json($response, ['status' => 'not-found']); - } - - // Otherwise proxy the request to container (assuming already up) - return $this->proxyToContainer($request, $response, $ip); -} diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 00b59e2..d736bce 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -1,13 +1,23 @@ // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ + app: { + baseURL: '/app/' // This replaces router.base from Nuxt 2 + }, + ssr: true, compatibilityDate: '2025-05-15', devtools: { enabled: true }, runtimeConfig: { public: { siteUrl: process.env.SITE_URL, backendUrl: process.env.BACKEND_URL, - apiVersion: process.env.API_VERSION, - apiUrl: `${process.env.BACKEND_URL}/${process.env.API_VERSION}` + apiUrl: `${process.env.BACKEND_URL}/api` + } + }, + nitro: { + prerender: { + crawlLinks: true, + routes: ['/', '/login'], // Add known routes + ignore: ['/*.php'], // Or leave empty to catch with [...slug].vue } }, css: [ diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index 731f261..e5f0169 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -2,44 +2,97 @@
+ + + +
+ \ No newline at end of file +.login-container { + max-width: 100%; + width: 400px; + margin: 5rem auto; + padding: 2rem; + border-radius: 8px; + background: #f9fafb; + box-shadow: 0 4px 10px rgb(0 0 0 / 0.1); +} + +.title { + text-align: center; + margin-bottom: 1.5rem; + font-weight: 700; + font-size: 1.8rem; + color: #222; +} +.input-group { + display: flex; + flex-direction: column; + margin-bottom: 1.2rem; +} +.btn { + width: 100%; + padding: 0.8rem; + font-weight: 600; + font-size: 1.1rem; + background-color: #3b82f6; /* blue-500 */ + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.3s; +} + +.btn:hover:not(:disabled) { + background-color: #2563eb; /* blue-600 */ +} + +.btn:disabled { + background-color: #93c5fd; /* blue-300 */ + cursor: not-allowed; +} +.error-text { + color: red; + font-size: 0.95rem; + margin-bottom: 1rem; + text-align: center; +} + diff --git a/frontend/pages/login.vue b/frontend/pages/login.vue deleted file mode 100644 index 611b2cc..0000000 --- a/frontend/pages/login.vue +++ /dev/null @@ -1,130 +0,0 @@ - - - - -