feat: change structure

This commit is contained in:
2025-07-22 12:51:53 +02:00
parent 070dfa2764
commit 8f5032e531
38 changed files with 432 additions and 279 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -6,7 +6,6 @@ This is a PHP-based API using the **Slim 4 Framework** that dynamically manages
## 🧩 Features
- Automatically creates and starts LXD containers per subdomain
- CAPTCHA validation for provisioning
- Proxies requests to containerized environments
- 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
backend/
├── app/
api/
│ ├── src/
│ │ ├── Controllers/
│ │ │ └── ProxyController.php
@ -39,7 +37,7 @@ backend/
## ⚙️ Requirements
- PHP 8.1+
- PHP 8.4+
- LXD installed and configured
- PHP-FPM + NGINX
- Composer
@ -61,13 +59,14 @@ backend/
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**
```bash
127.0.0.1 test.lxdapp.local
127.0.0.1 testone.lxdapp.local
5. **Make sure LXD is working**
```bash
@ -75,14 +74,14 @@ backend/
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.
7. **🧪 API Usage**
POST /api/v1/proxy
POST /api
**Request Body:**
```bash
@ -93,7 +92,7 @@ backend/
**Headers:**
Origin: http://customer.lxdapp.local
Origin: http://testone.lxdapp.local
**Response:**
```bash

3
api/config.json Normal file
View File

@ -0,0 +1,3 @@
{
"lxdapp.local": "testone"
}

View File

@ -2,7 +2,7 @@ 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
root /var/www/html/lxd-app; # Adjust this to your Slim project's public folder
index index.php index.html index.htm;
# Reverse proxy to Vue.js for /app/ route
@ -17,20 +17,18 @@ server {
# 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;
# try_files $uri $uri/ /index.php?$query_string;
try_files $uri /index.php?$query_string;
}
# Pass PHP scripts to PHP-FPM
location ~ \.php$ {
#location ~ \.php$ {
location = /index.php {
include snippets/fastcgi-php.conf;
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;
}

91
api/public/index.php Normal file
View 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();

View File

@ -48,8 +48,9 @@ class LoginController
// Write log
LogWriterHelper::write($name, $request->getUri());
$redirect = '/waiting?name='.$name .'&redirect='.urlencode($params['redirect']);
// 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) {
return $this->json($response, [
'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.
*/

View File

@ -22,10 +22,9 @@ class LxdService
* @throws Exception
*/
private function request(string $method, string $endpoint, array $body = []): array {
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");
}
// 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");
// }
$ch = curl_init("{$this->baseUrl}{$endpoint}");
@ -78,6 +77,7 @@ class LxdService
return $this->request('GET', "/1.0/instances/$name/state");
} catch (\Throwable $e) {
return null;
// echo 'Error: ' . $e->getMessage();
}
}
@ -99,7 +99,6 @@ class LxdService
* @throws Exception
*/
public function startContainer(string $name): array {
$startResponse = $this->request('PUT', "/1.0/instances/$name/state", [
"action" => "start",
"timeout" => 30,
@ -107,41 +106,10 @@ class LxdService
]);
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'];
// 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.");
return $startResponse;
}
/**
@ -194,11 +162,11 @@ class LxdService
return null;
}
public function isServiceRunning(string $container, string $service): bool
public function isServiceRunning(string $container, string $service): string
{
$lxcPath = $_ENV['LXC_PATH'] ?: 'lxc';
$cmd = "$lxcPath exec {$container} -- systemctl is-active {$service} 2>&1";
$output = shell_exec($cmd);
return trim($output) === 'active';
return trim($output);
}
}

View File

@ -6,7 +6,7 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API]
## 🌐 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
- Upon successful CAPTCHA, a request is made to the backend to:
- 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
- CAPTCHA login screen for triggering container creation
- 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
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**
frontend/
app/
├── pages/
│ └── index.vue # Login screen
├── plugins/
@ -49,3 +54,39 @@ frontend/
├── package.json
└── 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
View 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;
}

View File

@ -13,13 +13,6 @@ export default defineNuxtConfig({
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: [
'~/assets/css/common.css' // Path to your global CSS file
],

View File

@ -78,7 +78,9 @@ onMounted(() => {
const submitForm = async () => {
loading.value = true;
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 {
const res = await $fetch(`${window.location.origin}/api/login`, {
@ -91,17 +93,16 @@ const submitForm = async () => {
password: password.value,
source: 'login',
panswer: captchaValue.value,
redirect: redirectTo.value,
redirect: encodedRedirect.toString()
},
credentials: 'include',
throwHttpErrors: false, // important: do NOT throw on 401/4xx
});
captchaError.value = '';
const encodedRedirect = new URL(redirectTo.value, window.location.origin);
encodedRedirect.searchParams.set('username', username.value);
encodedRedirect.searchParams.set('password', password.value);
window.location.href = encodedRedirect.toString();
router.push(res.redirect);
// window.location.href = encodedRedirect.toString();
} catch (error) {
if (error.statusCode === 400) {
captchaError.value = error?.data?.message || 'Invalid CAPTCHA';
@ -121,17 +122,6 @@ const submitForm = async () => {
<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 {
max-width: 100%;
width: 400px;
@ -141,14 +131,6 @@ const submitForm = async () => {
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;
@ -175,12 +157,6 @@ const submitForm = async () => {
background-color: #93c5fd; /* blue-300 */
cursor: not-allowed;
}
.error-text {
color: red;
font-size: 0.95rem;
margin-bottom: 1rem;
text-align: center;
}
.input-group input {
padding: 0.8rem;
border: 1px solid #ccc;

74
app/pages/waiting.vue Normal file
View 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>

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,3 +0,0 @@
{
"testone.lxdapp.local": "testone"
}

View File

@ -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();

View File

@ -1,9 +0,0 @@
*{
box-sizing: border-box;
}
body{
position: relative;
padding: 0px;
margin: 0px;
overflow-x: hidden;
}

92
index.php Normal file
View 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);
}