diff --git a/api/public/index.php b/api/public/index.php
index 1a7b1ae..9fef4d0 100644
--- a/api/public/index.php
+++ b/api/public/index.php
@@ -10,6 +10,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use App\Controllers\CaptchaController;
use App\Controllers\LoginController;
+use App\Controllers\HandoffController;
use App\Services\LxdService;
use Zounar\PHPProxy\Proxy;
use App\Utils\LogWriterHelper;
@@ -71,7 +72,7 @@ $app->group('/api', function ($group) {
$group->get('/captcha', [CaptchaController::class, 'get']);
$group->post('/login', [LoginController::class, 'index']);
$group->get('/status', [LoginController::class, 'status']);
-
+ $group->get('/handoff/post', [HandoffController::class, 'post']);
});
/**
diff --git a/api/src/Controllers/HandoffController.php b/api/src/Controllers/HandoffController.php
new file mode 100644
index 0000000..744f69a
--- /dev/null
+++ b/api/src/Controllers/HandoffController.php
@@ -0,0 +1,85 @@
+getQueryParams();
+ $name = (string)($q['name'] ?? '');
+ $id = (string)($q['handoff'] ?? '');
+ $path = (string)($q['path'] ?? '/login');
+
+ if (!$name || !$id) {
+ return $this->html($res, 400, '
Bad request
');
+ }
+
+ $lxd = new LxdService();
+ $ip = $lxd->getContainerIP($name);
+ if (!$ip) {
+ return $this->html($res, 503, 'Container not ready
');
+ }
+
+ $key = "handoff:$name:$id";
+ $data = $_SESSION[$key] ?? null;
+ unset($_SESSION[$key]); // one-time use
+
+ if (!$data || empty($data['username']) || empty($data['password'])) {
+ return $this->html($res, 410, 'Handoff expired
');
+ }
+
+ // Restrict to relative paths
+ $path = $this->sanitizePath($path);
+
+ // If your container has TLS, prefer https://
+ $action = 'http://' . $ip . $path;
+
+ $html = <<
+
+ Signing you in…
+
+
+
+
+
+
+HTML;
+
+ return $this->html($res, 200, $html);
+ }
+
+ private function e(string $s): string
+ {
+ return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+ }
+
+ private function html(ResponseInterface $res, int $code, string $html): ResponseInterface
+ {
+ $res->getBody()->write($html);
+ return $res->withHeader('Content-Type', 'text/html; charset=utf-8')->withStatus($code);
+ }
+
+ private function sanitizePath(string $raw): string
+ {
+ if (preg_match('#^https?://#i', $raw)) return '/login';
+ if (!str_starts_with($raw, '/')) return '/login';
+ $path = parse_url($raw, PHP_URL_PATH) ?? '/login';
+ $allow = ['/', '/login', '/signin'];
+ return in_array($path, $allow, true) ? $path : '/login';
+ }
+}
+
\ No newline at end of file
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 9ad7fd2..61eed19 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -2,39 +2,41 @@
-
-
+
-
+
Access Denied
You must access this page through the proper login flow.
@@ -44,83 +46,83 @@
-
+.error-text { color: #b91c1c; margin-top: .5rem; }
+
\ No newline at end of file
diff --git a/app/pages/waiting.vue b/app/pages/waiting.vue
index 211c614..5e294a4 100644
--- a/app/pages/waiting.vue
+++ b/app/pages/waiting.vue
@@ -3,7 +3,7 @@
Container Status: {{ status }}
- Your container is ready! Open it
+ Your container is ready! Open it
Waiting for services to be ready...
Starting container...
@@ -14,13 +14,25 @@