forked from urvishpatelce/lxd-app
feat: change structure
This commit is contained in:
@ -6,7 +6,6 @@ This is a PHP-based API using the **Slim 4 Framework** that dynamically manages
|
|||||||
|
|
||||||
## 🧩 Features
|
## 🧩 Features
|
||||||
|
|
||||||
- Automatically creates and starts LXD containers per subdomain
|
|
||||||
- CAPTCHA validation for provisioning
|
- CAPTCHA validation for provisioning
|
||||||
- Proxies requests to containerized environments
|
- Proxies requests to containerized environments
|
||||||
- Persists domain-to-container mapping
|
- Persists domain-to-container mapping
|
||||||
@ -17,8 +16,7 @@ This is a PHP-based API using the **Slim 4 Framework** that dynamically manages
|
|||||||
|
|
||||||
## 📁 Project Structure
|
## 📁 Project Structure
|
||||||
|
|
||||||
backend/
|
api/
|
||||||
├── app/
|
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── Controllers/
|
│ │ ├── Controllers/
|
||||||
│ │ │ └── ProxyController.php
|
│ │ │ └── ProxyController.php
|
||||||
@ -39,7 +37,7 @@ backend/
|
|||||||
|
|
||||||
## ⚙️ Requirements
|
## ⚙️ Requirements
|
||||||
|
|
||||||
- PHP 8.1+
|
- PHP 8.4+
|
||||||
- LXD installed and configured
|
- LXD installed and configured
|
||||||
- PHP-FPM + NGINX
|
- PHP-FPM + NGINX
|
||||||
- Composer
|
- Composer
|
||||||
@ -61,13 +59,14 @@ backend/
|
|||||||
|
|
||||||
3. **Ensure PHP and NGINX are configured**
|
3. **Ensure PHP and NGINX are configured**
|
||||||
|
|
||||||
Check in backend/nginx.conf
|
NGINX config example:
|
||||||
|
```bash
|
||||||
|
Please check nginx.conf file in root
|
||||||
|
|
||||||
|
|
||||||
4. **Map domain in /etc/hosts**
|
4. **Map domain in /etc/hosts**
|
||||||
```bash
|
```bash
|
||||||
127.0.0.1 test.lxdapp.local
|
127.0.0.1 testone.lxdapp.local
|
||||||
|
|
||||||
5. **Make sure LXD is working**
|
5. **Make sure LXD is working**
|
||||||
```bash
|
```bash
|
||||||
@ -75,14 +74,14 @@ backend/
|
|||||||
|
|
||||||
6. **🔐 CAPTCHA Protection**
|
6. **🔐 CAPTCHA Protection**
|
||||||
|
|
||||||
The /api/v1/proxy POST endpoint expects a field panswer with the correct CAPTCHA.
|
The /api/ POST endpoint expects a field panswer with the correct CAPTCHA.
|
||||||
|
|
||||||
You can configure PCaptcha class for custom logic.
|
You can configure PCaptcha class for custom logic.
|
||||||
|
|
||||||
|
|
||||||
7. **🧪 API Usage**
|
7. **🧪 API Usage**
|
||||||
|
|
||||||
POST /api/v1/proxy
|
POST /api
|
||||||
|
|
||||||
**Request Body:**
|
**Request Body:**
|
||||||
```bash
|
```bash
|
||||||
@ -93,7 +92,7 @@ backend/
|
|||||||
|
|
||||||
**Headers:**
|
**Headers:**
|
||||||
|
|
||||||
Origin: http://customer.lxdapp.local
|
Origin: http://testone.lxdapp.local
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
```bash
|
```bash
|
||||||
3
api/config.json
Normal file
3
api/config.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"lxdapp.local": "testone"
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
server_name *.lxdapp.local lxdapp.local;
|
server_name *.lxdapp.local lxdapp.local;
|
||||||
|
|
||||||
root /var/www/html/lxd-app/backend/app/public; # Adjust this to your Slim project's public folder
|
root /var/www/html/lxd-app; # Adjust this to your Slim project's public folder
|
||||||
index index.php index.html index.htm;
|
index index.php index.html index.htm;
|
||||||
|
|
||||||
# Reverse proxy to Vue.js for /app/ route
|
# Reverse proxy to Vue.js for /app/ route
|
||||||
@ -17,20 +17,18 @@ server {
|
|||||||
# rewrite ^/app/(/.*)$ $1 break;
|
# 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
|
# Handle PHP Slim
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.php?$query_string;
|
# try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pass PHP scripts to PHP-FPM
|
# Pass PHP scripts to PHP-FPM
|
||||||
location ~ \.php$ {
|
#location ~ \.php$ {
|
||||||
|
location = /index.php {
|
||||||
include snippets/fastcgi-php.conf;
|
include snippets/fastcgi-php.conf;
|
||||||
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
}
|
}
|
||||||
|
|
||||||
91
api/public/index.php
Normal file
91
api/public/index.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
use App\Middleware\CorsMiddleware;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use App\Controllers\CaptchaController;
|
||||||
|
use App\Controllers\LoginController;
|
||||||
|
use App\Services\LxdService;
|
||||||
|
use Zounar\PHPProxy\Proxy;
|
||||||
|
use App\Utils\LogWriterHelper;
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// 🔹 Load .env config
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$domain = $_ENV['MAIN_COOKIE_DOMAIN'] ?? '.lxdapp.local';
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => $domain,
|
||||||
|
'secure' => false, // set true if using HTTPS
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Container using PHP-DI
|
||||||
|
$containerBuilder = new ContainerBuilder();
|
||||||
|
$containerBuilder->useAutowiring(true); // Enable autowiring globally
|
||||||
|
|
||||||
|
|
||||||
|
// Add settings
|
||||||
|
$settings = require __DIR__ . '/../src/Settings/Settings.php';
|
||||||
|
$settings($containerBuilder);
|
||||||
|
|
||||||
|
// Add dependencies
|
||||||
|
$dependencies = require __DIR__ . '/../src/Dependencies/Dependencies.php';
|
||||||
|
$dependencies($containerBuilder);
|
||||||
|
|
||||||
|
// Build the container
|
||||||
|
$container = $containerBuilder->build();
|
||||||
|
|
||||||
|
// Set container to AppFactory
|
||||||
|
AppFactory::setContainer($container);
|
||||||
|
|
||||||
|
// Create App
|
||||||
|
$app = AppFactory::create();
|
||||||
|
|
||||||
|
// 🔹 CORS middleware
|
||||||
|
$app->add(CorsMiddleware::class);
|
||||||
|
|
||||||
|
|
||||||
|
// Register middleware
|
||||||
|
(require __DIR__ . '/../src/Bootstrap/Middleware.php')($app);
|
||||||
|
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
|
||||||
|
// API contianer proxy route
|
||||||
|
$app->group('/api', function ($group) {
|
||||||
|
$group->get('/captcha', [CaptchaController::class, 'get']);
|
||||||
|
$group->post('/login', [LoginController::class, 'index']);
|
||||||
|
$group->get('/status', [LoginController::class, 'status']);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON response helper
|
||||||
|
*/
|
||||||
|
function jsonResponse(Response $response, array $data, int $status = 200): Response {
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run app
|
||||||
|
$app->run();
|
||||||
@ -48,8 +48,9 @@ class LoginController
|
|||||||
// Write log
|
// Write log
|
||||||
LogWriterHelper::write($name, $request->getUri());
|
LogWriterHelper::write($name, $request->getUri());
|
||||||
|
|
||||||
|
$redirect = '/waiting?name='.$name .'&redirect='.urlencode($params['redirect']);
|
||||||
// Login success
|
// Login success
|
||||||
return $this->json($response, ['status' => 'success', 'message' => 'Container started!']);
|
return $this->json($response, ['status' => 'success', 'message' => 'Container started!', 'redirect' => $redirect]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return $this->json($response, [
|
return $this->json($response, [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
@ -58,6 +59,68 @@ class LoginController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function status(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||||
|
{
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
$name = $queryParams['name'] ?? '';
|
||||||
|
|
||||||
|
if (empty($name)) {
|
||||||
|
return $this->json($response, [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Missing container name.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lxd = new LxdService();
|
||||||
|
|
||||||
|
$state = $lxd->getContainerState($name);
|
||||||
|
if (!$state) {
|
||||||
|
return $this->json($response, [
|
||||||
|
'status' => 'not_found',
|
||||||
|
'message' => 'Container not found',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = $state['metadata']['status'] ?? 'Stopped';
|
||||||
|
if ($status !== 'Running') {
|
||||||
|
return $this->json($response, [
|
||||||
|
'status' => 'starting',
|
||||||
|
'message' => 'Container is not yet running',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ip = $lxd->getContainerIP($name);
|
||||||
|
$nginx = $lxd->isServiceRunning($name, 'nginx');
|
||||||
|
$mysql = $lxd->isServiceRunning($name, 'mysql');
|
||||||
|
if ($ip && $nginx === 'active' && $mysql === 'active') {
|
||||||
|
return $this->json($response, [
|
||||||
|
'status' => 'ready',
|
||||||
|
'ip' => $ip,
|
||||||
|
'message' => 'Container is ready',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($nginx === 'failed'){
|
||||||
|
return $this->json($response, [
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => 'Failed to start web server service in container',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($mysql === 'failed'){
|
||||||
|
return $this->json($response, [
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => 'Failed to start mysql service in container',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json($response, [
|
||||||
|
'status' => 'running',
|
||||||
|
'message' => 'Container is running, waiting for services...',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a JSON response.
|
* Sends a JSON response.
|
||||||
*/
|
*/
|
||||||
@ -22,10 +22,9 @@ class LxdService
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
private function request(string $method, string $endpoint, array $body = []): array {
|
private function request(string $method, string $endpoint, array $body = []): array {
|
||||||
|
// if (!isset($_ENV['LXD_CLIENT_CERT'], $_ENV['LXD_CLIENT_KEY'])) {
|
||||||
if (!isset($_ENV['LXD_CLIENT_CERT'], $_ENV['LXD_CLIENT_KEY'])) {
|
// throw new \Exception("LXD_CLIENT_CERT and LXD_CLIENT_KEY must be set in .env");
|
||||||
throw new \Exception("LXD_CLIENT_CERT and LXD_CLIENT_KEY must be set in .env");
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
$ch = curl_init("{$this->baseUrl}{$endpoint}");
|
$ch = curl_init("{$this->baseUrl}{$endpoint}");
|
||||||
|
|
||||||
@ -78,6 +77,7 @@ class LxdService
|
|||||||
return $this->request('GET', "/1.0/instances/$name/state");
|
return $this->request('GET', "/1.0/instances/$name/state");
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return null;
|
return null;
|
||||||
|
// echo 'Error: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +99,6 @@ class LxdService
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function startContainer(string $name): array {
|
public function startContainer(string $name): array {
|
||||||
|
|
||||||
$startResponse = $this->request('PUT', "/1.0/instances/$name/state", [
|
$startResponse = $this->request('PUT', "/1.0/instances/$name/state", [
|
||||||
"action" => "start",
|
"action" => "start",
|
||||||
"timeout" => 30,
|
"timeout" => 30,
|
||||||
@ -107,41 +106,10 @@ class LxdService
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!isset($startResponse['operation'])) {
|
if (!isset($startResponse['operation'])) {
|
||||||
throw new \Exception("No operation returned from start request");
|
throw new \Exception("Failed to start container, please try again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$startOpUrl = $startResponse['operation'];
|
return $startResponse;
|
||||||
|
|
||||||
// 5. Wait for start operation to complete
|
|
||||||
do {
|
|
||||||
sleep(1);
|
|
||||||
$startOpResp = $this->request('GET', $startOpUrl);
|
|
||||||
$startStatus = $startOpResp['metadata']['status_code'] ?? 0;
|
|
||||||
} while ($startStatus < 200);
|
|
||||||
|
|
||||||
if ($startStatus >= 400) {
|
|
||||||
throw new \Exception("Failed to start container, Please try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep(5);
|
|
||||||
|
|
||||||
// Poll services (Nginx and MySQL) for up to 30 seconds
|
|
||||||
$maxRetries = 30;
|
|
||||||
$retry = 0;
|
|
||||||
|
|
||||||
while ($retry < $maxRetries) {
|
|
||||||
$nginxReady = $this->isServiceRunning($name, 'nginx');
|
|
||||||
$mysqlReady = $this->isServiceRunning($name, 'mysql');
|
|
||||||
|
|
||||||
if ($nginxReady && $mysqlReady) {
|
|
||||||
return $startResponse; // ✅ All good
|
|
||||||
}
|
|
||||||
|
|
||||||
$retry++;
|
|
||||||
sleep(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \Exception("Container started but services not ready.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -194,11 +162,11 @@ class LxdService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isServiceRunning(string $container, string $service): bool
|
public function isServiceRunning(string $container, string $service): string
|
||||||
{
|
{
|
||||||
$lxcPath = $_ENV['LXC_PATH'] ?: 'lxc';
|
$lxcPath = $_ENV['LXC_PATH'] ?: 'lxc';
|
||||||
$cmd = "$lxcPath exec {$container} -- systemctl is-active {$service} 2>&1";
|
$cmd = "$lxcPath exec {$container} -- systemctl is-active {$service} 2>&1";
|
||||||
$output = shell_exec($cmd);
|
$output = shell_exec($cmd);
|
||||||
return trim($output) === 'active';
|
return trim($output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0
frontend/.gitignore → app/.gitignore
vendored
0
frontend/.gitignore → app/.gitignore
vendored
@ -6,7 +6,7 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API]
|
|||||||
|
|
||||||
## 🌐 Overview
|
## 🌐 Overview
|
||||||
|
|
||||||
- Each user has a subdomain (e.g., `mitul.lxdapp.local`)
|
- Each user has a subdomain (e.g., `testone.lxdapp.local`)
|
||||||
- On visiting the subdomain, a login page prompts for CAPTCHA
|
- On visiting the subdomain, a login page prompts for CAPTCHA
|
||||||
- Upon successful CAPTCHA, a request is made to the backend to:
|
- Upon successful CAPTCHA, a request is made to the backend to:
|
||||||
- Create or start an LXD container
|
- Create or start an LXD container
|
||||||
@ -20,7 +20,7 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API]
|
|||||||
- Subdomain-aware dynamic environment provisioning
|
- Subdomain-aware dynamic environment provisioning
|
||||||
- CAPTCHA login screen for triggering container creation
|
- CAPTCHA login screen for triggering container creation
|
||||||
- API integration with backend (Slim PHP) service
|
- API integration with backend (Slim PHP) service
|
||||||
- Axios-based POST to `/api/v1/proxy`
|
- Axios-based POST to `/api/`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -34,11 +34,16 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API]
|
|||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
|
3. **Visit the subdomain**
|
||||||
|
|
||||||
|
http://testone.lxdapp.local:3000
|
||||||
|
|
||||||
|
⚠️ Make sure this domain is mapped in /etc/hosts and that the backend is running on the correct origin.
|
||||||
|
|
||||||
|
|
||||||
**🗂 Project Structure**
|
**🗂 Project Structure**
|
||||||
|
|
||||||
frontend/
|
app/
|
||||||
├── pages/
|
├── pages/
|
||||||
│ └── index.vue # Login screen
|
│ └── index.vue # Login screen
|
||||||
├── plugins/
|
├── plugins/
|
||||||
@ -49,3 +54,39 @@ frontend/
|
|||||||
├── package.json
|
├── package.json
|
||||||
└── README.md
|
└── README.md
|
||||||
|
|
||||||
|
|
||||||
|
**🔐 CAPTCHA Flow**
|
||||||
|
|
||||||
|
User opens subdomain: testone.lxdapp.local
|
||||||
|
|
||||||
|
Enters the CAPTCHA value
|
||||||
|
|
||||||
|
Form submits:
|
||||||
|
|
||||||
|
POST /api/login with JSON payload:
|
||||||
|
|
||||||
|
{
|
||||||
|
"source": "login",
|
||||||
|
"panswer": "abc123"
|
||||||
|
}
|
||||||
|
|
||||||
|
Adds header:
|
||||||
|
|
||||||
|
Origin: http://testone.lxdapp.local
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
|
||||||
|
Validates CAPTCHA
|
||||||
|
|
||||||
|
Creates or starts container
|
||||||
|
|
||||||
|
Responds with container IP proxy
|
||||||
|
|
||||||
|
|
||||||
|
**🧪 Development Notes**
|
||||||
|
|
||||||
|
config.json in backend maps subdomain to LXD container
|
||||||
|
|
||||||
|
This frontend app assumes the backend is on the same subdomain
|
||||||
|
|
||||||
|
If needed, configure proxy in nuxt.config.js or use relative API paths
|
||||||
33
app/assets/css/common.css
Normal file
33
app/assets/css/common.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
*{
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body{
|
||||||
|
position: relative;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.page-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
.error-text {
|
||||||
|
color: red;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@ -13,13 +13,6 @@ export default defineNuxtConfig({
|
|||||||
apiUrl: `${process.env.BACKEND_URL}/api`
|
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: [
|
css: [
|
||||||
'~/assets/css/common.css' // Path to your global CSS file
|
'~/assets/css/common.css' // Path to your global CSS file
|
||||||
],
|
],
|
||||||
@ -78,7 +78,9 @@ onMounted(() => {
|
|||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
output.value = '';
|
output.value = '';
|
||||||
const config = useRuntimeConfig();
|
const encodedRedirect = new URL(redirectTo.value, window.location.origin);
|
||||||
|
encodedRedirect.searchParams.set('username', username.value);
|
||||||
|
encodedRedirect.searchParams.set('password', password.value);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await $fetch(`${window.location.origin}/api/login`, {
|
const res = await $fetch(`${window.location.origin}/api/login`, {
|
||||||
@ -91,17 +93,16 @@ const submitForm = async () => {
|
|||||||
password: password.value,
|
password: password.value,
|
||||||
source: 'login',
|
source: 'login',
|
||||||
panswer: captchaValue.value,
|
panswer: captchaValue.value,
|
||||||
redirect: redirectTo.value,
|
redirect: encodedRedirect.toString()
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
throwHttpErrors: false, // important: do NOT throw on 401/4xx
|
throwHttpErrors: false, // important: do NOT throw on 401/4xx
|
||||||
});
|
});
|
||||||
|
|
||||||
captchaError.value = '';
|
captchaError.value = '';
|
||||||
const encodedRedirect = new URL(redirectTo.value, window.location.origin);
|
|
||||||
encodedRedirect.searchParams.set('username', username.value);
|
router.push(res.redirect);
|
||||||
encodedRedirect.searchParams.set('password', password.value);
|
// window.location.href = encodedRedirect.toString();
|
||||||
window.location.href = encodedRedirect.toString();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.statusCode === 400) {
|
if (error.statusCode === 400) {
|
||||||
captchaError.value = error?.data?.message || 'Invalid CAPTCHA';
|
captchaError.value = error?.data?.message || 'Invalid CAPTCHA';
|
||||||
@ -121,17 +122,6 @@ const submitForm = async () => {
|
|||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.login-container {
|
.login-container {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
@ -141,14 +131,6 @@ const submitForm = async () => {
|
|||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
box-shadow: 0 4px 10px rgb(0 0 0 / 0.1);
|
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 {
|
.input-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -175,12 +157,6 @@ const submitForm = async () => {
|
|||||||
background-color: #93c5fd; /* blue-300 */
|
background-color: #93c5fd; /* blue-300 */
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.error-text {
|
|
||||||
color: red;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.input-group input {
|
.input-group input {
|
||||||
padding: 0.8rem;
|
padding: 0.8rem;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
74
app/pages/waiting.vue
Normal file
74
app/pages/waiting.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="waiting-container">
|
||||||
|
<h2>Container Status: {{ status }}</h2>
|
||||||
|
<p v-if="status === 'ready'">
|
||||||
|
Your container is ready! <a :href="`http://${ip}`" target="_blank">Open it</a>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="status === 'running'">Waiting for services to be ready...</p>
|
||||||
|
<p v-else-if="status === 'starting'">Starting container...</p>
|
||||||
|
<p v-else-if="status === 'error'" class="error-text">{{ errorMessage }}</p>
|
||||||
|
</div>
|
||||||
|
<Spinner :loading="loading" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const name = route.query.name;
|
||||||
|
const redirect = route.query.redirect;
|
||||||
|
const loading = ref(true);
|
||||||
|
const status = ref('starting');
|
||||||
|
const ip = ref(null);
|
||||||
|
const interval = ref(null);
|
||||||
|
const errorMessage = ref(null);
|
||||||
|
|
||||||
|
const checkStatus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await $fetch(`${window.location.origin}/api/status?name=${name}`);
|
||||||
|
status.value = res.status;
|
||||||
|
|
||||||
|
if (res.status === 'ready') {
|
||||||
|
ip.value = res.ip;
|
||||||
|
clearInterval(interval.value);
|
||||||
|
loading.value = false;
|
||||||
|
// Redirect or show login link
|
||||||
|
window.location.href= redirect ?? `http://${res.ip}`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
loading.value = false;
|
||||||
|
status.value = 'error';
|
||||||
|
errorMessage.value = error?.data?.message || 'Internal server error!';
|
||||||
|
clearInterval(interval.value);
|
||||||
|
setTimeout( () => {
|
||||||
|
router.replace(`/?auth=ok&redirect=${redirect}`);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkStatus();
|
||||||
|
interval.value = setInterval(checkStatus, 3000); // Poll every 3s
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(interval.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.waiting-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);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"testone.lxdapp.local": "testone"
|
|
||||||
}
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use DI\ContainerBuilder;
|
|
||||||
use Slim\Factory\AppFactory;
|
|
||||||
use Dotenv\Dotenv;
|
|
||||||
use App\Middleware\CorsMiddleware;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
|
||||||
use App\Controllers\CaptchaController;
|
|
||||||
use App\Controllers\LoginController;
|
|
||||||
use App\Services\LxdService;
|
|
||||||
use Zounar\PHPProxy\Proxy;
|
|
||||||
use App\Utils\LogWriterHelper;
|
|
||||||
|
|
||||||
require __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
// 🔹 Load .env config
|
|
||||||
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
|
|
||||||
$dotenv->load();
|
|
||||||
|
|
||||||
$domain = $_ENV['MAIN_COOKIE_DOMAIN'] ?? '.lxdapp.local';
|
|
||||||
session_set_cookie_params([
|
|
||||||
'lifetime' => 0,
|
|
||||||
'path' => '/',
|
|
||||||
'domain' => $domain,
|
|
||||||
'secure' => false, // set true if using HTTPS
|
|
||||||
'httponly' => true,
|
|
||||||
'samesite' => 'Lax',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build Container using PHP-DI
|
|
||||||
$containerBuilder = new ContainerBuilder();
|
|
||||||
$containerBuilder->useAutowiring(true); // Enable autowiring globally
|
|
||||||
|
|
||||||
|
|
||||||
// Add settings
|
|
||||||
$settings = require __DIR__ . '/../src/Settings/Settings.php';
|
|
||||||
$settings($containerBuilder);
|
|
||||||
|
|
||||||
// Add dependencies
|
|
||||||
$dependencies = require __DIR__ . '/../src/Dependencies/Dependencies.php';
|
|
||||||
$dependencies($containerBuilder);
|
|
||||||
|
|
||||||
// Build the container
|
|
||||||
$container = $containerBuilder->build();
|
|
||||||
|
|
||||||
// Set container to AppFactory
|
|
||||||
AppFactory::setContainer($container);
|
|
||||||
|
|
||||||
// Create App
|
|
||||||
$app = AppFactory::create();
|
|
||||||
|
|
||||||
// 🔹 CORS middleware
|
|
||||||
$app->add(CorsMiddleware::class);
|
|
||||||
|
|
||||||
|
|
||||||
// Register middleware
|
|
||||||
(require __DIR__ . '/../src/Bootstrap/Middleware.php')($app);
|
|
||||||
|
|
||||||
|
|
||||||
// Register routes
|
|
||||||
|
|
||||||
// API contianer proxy route
|
|
||||||
$app->group('/api', function ($group) {
|
|
||||||
$group->get('/captcha', [CaptchaController::class, 'get']);
|
|
||||||
$group->post('/login', [LoginController::class, 'index']);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
$app->any('/{routes:.*}', function (Request $request, Response $response, array $args) {
|
|
||||||
try {
|
|
||||||
$mainDomain = $_ENV['MAIN_DOMAIN'] ?? 'lxdapp.local';
|
|
||||||
|
|
||||||
$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();
|
|
||||||
|
|
||||||
if (!$name) {
|
|
||||||
return jsonResponse($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$lxd->containerExists($name)) {
|
|
||||||
return jsonResponse($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$containerInfo = $lxd->getContainerState($name);
|
|
||||||
$status = $containerInfo['metadata']['status'] ?? 'Stopped';
|
|
||||||
|
|
||||||
if ($status !== 'Running') {
|
|
||||||
$redirectUrl = (string) $request->getUri();
|
|
||||||
return $response
|
|
||||||
->withHeader('Location', 'app/?auth=ok&redirect=' . urlencode($redirectUrl))
|
|
||||||
->withStatus(302);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ip = $lxd->getContainerIP($name);
|
|
||||||
if (!$ip) {
|
|
||||||
return jsonResponse($response, ['status' => 'error', 'message' => 'Could not fetch container IP'], 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// BEGIN Proxy logic
|
|
||||||
$baseUrl = "http://$ip/";
|
|
||||||
$uri = $request->getUri();
|
|
||||||
$path = $uri->getPath();
|
|
||||||
|
|
||||||
$prefix = '/api/';
|
|
||||||
if (strpos($path, $prefix) === 0) {
|
|
||||||
$path = substr($path, strlen($prefix));
|
|
||||||
if ($path === '') {
|
|
||||||
$path = '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = $uri->getQuery();
|
|
||||||
$targetUrl = $baseUrl . $path . ($query ? '?' . $query : '');
|
|
||||||
|
|
||||||
Proxy::$AUTH_KEY = $_ENV['AUTH_KEY'] ?? 'YOUR_DEFAULT_AUTH_KEY';
|
|
||||||
Proxy::$ENABLE_AUTH = true;
|
|
||||||
Proxy::$HEADER_HTTP_PROXY_AUTH = 'HTTP_PROXY_AUTH';
|
|
||||||
|
|
||||||
$_SERVER['HTTP_PROXY_AUTH'] = Proxy::$AUTH_KEY;
|
|
||||||
$_SERVER['HTTP_PROXY_TARGET_URL'] = $targetUrl;
|
|
||||||
|
|
||||||
$responseCode = Proxy::run();
|
|
||||||
|
|
||||||
LogWriterHelper::write($name, $targetUrl);
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return jsonResponse($response, [
|
|
||||||
'status' => 'error',
|
|
||||||
'message' => 'Internal Server Error: ' . $e->getMessage()
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON response helper
|
|
||||||
*/
|
|
||||||
function jsonResponse(Response $response, array $data, int $status = 200): Response {
|
|
||||||
$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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run app
|
|
||||||
$app->run();
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
*{
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body{
|
|
||||||
position: relative;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
92
index.php
Normal file
92
index.php
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// === Handle API requests ===
|
||||||
|
$requestUri = $_SERVER['REQUEST_URI'];
|
||||||
|
if (str_starts_with($requestUri, '/api/')) {
|
||||||
|
require __DIR__ . '/api/public/index.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Setup ===
|
||||||
|
require __DIR__ . "/api/vendor/autoload.php";
|
||||||
|
require __DIR__ . "/api/src/Services/LxdService.php";
|
||||||
|
|
||||||
|
use Zounar\PHPProxy\Proxy;
|
||||||
|
use App\Services\LxdService;
|
||||||
|
|
||||||
|
|
||||||
|
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
// Load container mapping from domain config
|
||||||
|
$config = json_decode(file_get_contents(__DIR__ . '/api/config.json'), true);
|
||||||
|
$container = $config[$host] ?? null;
|
||||||
|
$lxd = new LxdService();
|
||||||
|
|
||||||
|
// === Helper URLs ===
|
||||||
|
$redirectBase = parseUrl("$host/app?auth=ok&redirect=" . urlencode(getFullUrl()));
|
||||||
|
$waitingPage = parseUrl("$host/app/waiting?name=$container&redirect=" . urlencode($requestUri));
|
||||||
|
|
||||||
|
// === If container is missing or invalid ===
|
||||||
|
if (!$container || !$lxd->containerExists($container)) {
|
||||||
|
redirect($redirectBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Check container status ===
|
||||||
|
$state = $lxd->getContainerState($container)['metadata']['status'] ?? 'Stopped';
|
||||||
|
|
||||||
|
if ($state !== 'Running') {
|
||||||
|
redirect($redirectBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Get container IP ===
|
||||||
|
$ip = $lxd->getContainerIP($container);
|
||||||
|
if (!$ip) {
|
||||||
|
redirect($waitingPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Proxy to container ===
|
||||||
|
proxy($container, "http://{$ip}{$requestUri}");
|
||||||
|
exit;
|
||||||
|
|
||||||
|
|
||||||
|
// === Functions ===
|
||||||
|
|
||||||
|
function redirect(string $to): void {
|
||||||
|
header("Location: $to", true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxy(string $name, string $targetUrl): void {
|
||||||
|
Proxy::$AUTH_KEY = $_ENV['AUTH_KEY'] ?? 'YOUR_DEFAULT_AUTH_KEY';
|
||||||
|
Proxy::$ENABLE_AUTH = true;
|
||||||
|
Proxy::$HEADER_HTTP_PROXY_AUTH = 'HTTP_PROXY_AUTH';
|
||||||
|
|
||||||
|
$_SERVER['HTTP_PROXY_AUTH'] = Proxy::$AUTH_KEY;
|
||||||
|
$_SERVER['HTTP_PROXY_TARGET_URL'] = $targetUrl;
|
||||||
|
|
||||||
|
$responseCode = Proxy::run();
|
||||||
|
writeLog($name, $targetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFullUrl(): string {
|
||||||
|
$protocol = (!empty($_SERVER['HTTPS']) || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
||||||
|
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||||
|
return $protocol . $host . $_SERVER['REQUEST_URI'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUrl(string $url = ''): string {
|
||||||
|
$scheme = $_SERVER['REQUEST_SCHEME'] ?? 'http';
|
||||||
|
return "$scheme://$url";
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLog(string $name, string $uri): void {
|
||||||
|
$logDir = __DIR__ . '/api/public/last-access-logs';
|
||||||
|
|
||||||
|
if (!is_dir($logDir)) {
|
||||||
|
mkdir($logDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logLine = date("Y-m-d H:i:s") . " : " . $uri . "\n";
|
||||||
|
file_put_contents("$logDir/$name.txt", $logLine, FILE_APPEND);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user