first commit

This commit is contained in:
Manesh 2026-04-13 16:42:01 +00:00
commit a5a052a3f1
11 changed files with 2075 additions and 0 deletions

6
.env Executable file
View File

@ -0,0 +1,6 @@
STATIC_IP=147.93.40.215
PARENT_PATH=/home/user/conf/web
PUBLIC_WEB_ROOT=/home/user/web
ERROR_LOG_ROOT=/var/log/apache2/domains
NGINX_RELOAD_CMD="systemctl reload nginx"
PORT=3013

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# Hestia-Nginx-Reverse-Proxy
# Hestia-Nginx-Reverse-Proxy

19
ReverseProxy/.env Executable file
View File

@ -0,0 +1,19 @@
# Networking / defaults
STATIC_IP=147.93.40.215
PARENT_PATH=/home/user/conf/web
PUBLIC_WEB_ROOT=/home/user/web
ERROR_LOG_ROOT=/var/log/apache2/domains
# Binaries
SUDO_BIN=/usr/bin/sudo
NGINX_BIN=/usr/sbin/nginx
SYSTEMCTL_BIN=/bin/systemctl
# API
API_KEY=change_me_to_a_long_random_value
DISABLE_AUTH=false
# App bind
BIND_HOST=0.0.0.0
BIND_PORT=9999

View File

@ -0,0 +1,435 @@
#!/usr/bin/env python3
"""
FastAPI-based Nginx reverse-proxy generator.
POST /api/nginx/app
Body:
{
"domain": "ebayapp.data4autos.com",
"port": 3011,
"ip": "147.93.40.215", # optional (defaults to STATIC_IP)
"dryRun": false, # optional
"skipReload": false # optional
}
Auth (required unless DISABLE_AUTH=true):
send header: X-API-Key: <API_KEY>
Run as root (recommended) or configure sudoers for nginx/systemctl.
"""
import os
import re
import time
import pathlib
import subprocess
from typing import Optional, Dict, Any
from fastapi import FastAPI, Body, HTTPException, Header, Depends
from fastapi.responses import JSONResponse
from pydantic import BaseModel, field_validator
# ---------------------------- Config ----------------------------
STATIC_IP = os.getenv("STATIC_IP", "147.93.40.215")
PARENT_PATH = os.getenv("PARENT_PATH", "/home/user/conf/web") # <parent>/<domain>/
PUBLIC_WEB_ROOT = os.getenv("PUBLIC_WEB_ROOT", "/home/user/web") # for /error alias
ERROR_LOG_ROOT = os.getenv("ERROR_LOG_ROOT", "/var/log/apache2/domains")
# Binaries (absolute paths are safer under systemd)
SUDO_BIN = os.getenv("SUDO_BIN", "/usr/bin/sudo")
NGINX_BIN = os.getenv("NGINX_BIN", "/usr/sbin/nginx")
SYSTEMCTL_BIN = os.getenv("SYSTEMCTL_BIN", "/bin/systemctl")
# Simple header auth
API_KEY = os.getenv("API_KEY", "")
DISABLE_AUTH = os.getenv("DISABLE_AUTH", "false").lower() in ("1", "true", "yes")
# Safety: bound interface/port for uvicorn (when you use the run command below)
BIND_HOST = os.getenv("BIND_HOST", "127.0.0.1") # expose via reverse proxy if you like
BIND_PORT = int(os.getenv("BIND_PORT", "5055"))
# ------------------------- App bootstrap ------------------------
app = FastAPI(title="Nginx Reverse Proxy Generator (FastAPI)")
# ------------------------- Validators ---------------------------
_ipv4_re = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$")
_domain_re = re.compile(r"^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,63}$")
def is_valid_ipv4(ip: str) -> bool:
if not _ipv4_re.match(ip):
return False
parts = ip.split(".")
try:
return all(0 <= int(p) <= 255 and str(int(p)) == p for p in parts)
except Exception:
return False
def is_valid_domain(d: str) -> bool:
return bool(_domain_re.match(d))
def is_valid_port(p: int) -> bool:
return isinstance(p, int) and 1 <= p <= 65535
def safe_domain(d: str) -> str:
"""Reject path traversal and uppercases; ensure it's a normal FQDN."""
if not is_valid_domain(d):
raise ValueError("Invalid domain")
return d.lower()
# --------------------------- Models -----------------------------
class GenRequest(BaseModel):
domain: str
port: int
ip: Optional[str] = None
dryRun: Optional[bool] = False
skipReload: Optional[bool] = False
@field_validator("domain")
@classmethod
def _dom(cls, v: str) -> str:
if not is_valid_domain(v):
raise ValueError("Invalid domain")
return v
@field_validator("port")
@classmethod
def _prt(cls, v: int) -> int:
if not is_valid_port(v):
raise ValueError("Invalid port")
return v
@field_validator("ip")
@classmethod
def _ip(cls, v: Optional[str]) -> Optional[str]:
if v is None or str(v).strip() == "":
return None
if not is_valid_ipv4(v):
raise ValueError("Invalid ip")
return v
# ------------------------- Auth dependency ----------------------
def require_api_key(x_api_key: Optional[str] = Header(default=None)) -> None:
return
if DISABLE_AUTH:
return
if not API_KEY:
# If you forgot to set API_KEY but disabled auth is False
raise HTTPException(status_code=500, detail="Server misconfigured: API_KEY not set")
if x_api_key != API_KEY:
print(x_api_key,"!=",API_KEY)
raise HTTPException(status_code=401, detail="Invalid or missing API key")
# ----------------------- Templating funcs -----------------------
# def build_ssl(ip: str, domain: str, port: int) -> str:
# base = f"{PARENT_PATH}/{domain}"
# public = f"{PUBLIC_WEB_ROOT}/{domain}"
# return f"""server {{
# listen {ip}:443 ssl;
# server_name {domain};
# error_log {ERROR_LOG_ROOT}/{domain}.error.log error;
# ssl_certificate {base}/ssl/{domain}.pem;
# ssl_certificate_key {base}/ssl/{domain}.key;
# location / {{
# proxy_pass http://127.0.0.1:{port};
# 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;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }}
# location ~ /\\.(?!well-known\\/|file) {{
# deny all;
# return 404;
# }}
# location /error/ {{
# alias {public}/document_errors/;
# }}
# disable_symlinks if_not_owner from={public}/public_html;
# include {base}/nginx.ssl.conf_*;
# }}
# """.rstrip() + "\n"
# def build_nonssl(ip: str, domain: str, port: int) -> str:
# base = f"{PARENT_PATH}/{domain}"
# public = f"{PUBLIC_WEB_ROOT}/{domain}"
# return f"""server {{
# listen {ip}:80;
# server_name {domain};
# error_log {ERROR_LOG_ROOT}/{domain}.error.log error;
# include {base}/nginx.forcessl.conf*;
# location / {{
# proxy_pass http://127.0.0.1:{port};
# 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;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }}
# location ~ /\\.(?!well-known\\/|file) {{
# deny all;
# return 404;
# }}
# location /error/ {{
# alias {public}/document_errors/;
# }}
# disable_symlinks if_not_owner from={public}/public_html;
# include {base}/nginx.conf_*;
# }}
# """.rstrip() + "\n"
def build_ssl(ip: str, domain: str, port: int) -> str:
base = f"{PARENT_PATH}/{domain}"
public = f"{PUBLIC_WEB_ROOT}/{domain}"
return f"""server {{
listen {ip}:443 ssl;
server_name {domain};
error_log {ERROR_LOG_ROOT}/{domain}.error.log error;
ssl_certificate {base}/ssl/{domain}.pem;
ssl_certificate_key {base}/ssl/{domain}.key;
ssl_stapling on;
ssl_stapling_verify on;
# TLS 1.3 0-RTT protection
if ($anti_replay = 307) {{ return 307 https://$host$request_uri; }}
if ($anti_replay = 425) {{ return 425; }}
include {base}/nginx.hsts.conf*;
#################################################################
# FORCE BROWSER TO UPGRADE HTTP REQUESTS TO HTTPS
#################################################################
add_header Content-Security-Policy "upgrade-insecure-requests";
#################################################################
# SECURITY
#################################################################
location ~ /\\.(?!well-known\\/|file) {{
deny all;
return 404;
}}
#################################################################
# MAIN REVERSE PROXY
#################################################################
location / {{
proxy_pass http://127.0.0.1:{port};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
# convert upstream http redirects into https
proxy_redirect http:// https://;
proxy_cache_bypass $http_upgrade;
proxy_request_buffering off;
proxy_connect_timeout 36000s;
proxy_read_timeout 36000s;
proxy_send_timeout 36000s;
send_timeout 36000s;
client_max_body_size 10240m;
}}
#################################################################
location /error/ {{
alias {public}/document_errors/;
}}
disable_symlinks if_not_owner from={public}/public_html;
proxy_hide_header Upgrade;
include {base}/nginx.ssl.conf_*;
}}
""".rstrip() + "\n"
def build_nonssl(ip: str, domain: str, port: int) -> str:
return f"""server {{
listen {ip}:80;
server_name {domain};
return 301 https://$host$request_uri;
}}
""".rstrip() + "\n"
# ----------------------- Shell helpers --------------------------
def run(cmd: list[str], timeout: int = 30) -> dict:
"""Run a command and return {ok, rc, stdout, stderr} without raising."""
try:
p = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout,
check=False,
text=True,
)
return {"ok": p.returncode == 0, "rc": p.returncode, "stdout": p.stdout, "stderr": p.stderr}
except subprocess.TimeoutExpired as te:
return {"ok": False, "rc": -1, "stdout": te.stdout or "", "stderr": f"Timeout: {te}"}
except Exception as e:
return {"ok": False, "rc": -2, "stdout": "", "stderr": str(e)}
# --------------------------- Routes -----------------------------
@app.get("/healthz")
def healthz():
return {"ok": True, "ip": STATIC_IP, "parent": PARENT_PATH}
@app.post("/api/nginx/app", dependencies=[Depends(require_api_key)])
def create_or_update_vhost(payload: GenRequest = Body(...)) -> JSONResponse:
# Resolve values
ip = payload.ip or STATIC_IP
domain = safe_domain(payload.domain)
port = int(payload.port)
# Render templates
ssl_txt = build_ssl(ip, domain, port)
nonssl_txt = build_nonssl(ip, domain, port)
# Target paths
domain_dir = pathlib.Path(PARENT_PATH) / domain
ssl_file = domain_dir / "nginx.ssl.conf"
nonssl_file = domain_dir / "nginx.conf"
if payload.dryRun:
return JSONResponse(
{
"ok": True,
"dryRun": True,
"files": {
str(nonssl_file): nonssl_txt,
str(ssl_file): ssl_txt,
},
}
)
# Ensure directory exists
try:
domain_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to create {domain_dir}: {e}")
# Check write access
if not os.access(domain_dir, os.W_OK | os.X_OK):
raise HTTPException(
status_code=403,
detail=f"No write access to {domain_dir}. Run as root or fix permissions.",
)
# Backup old files, if present
def backup_if_exists(p: pathlib.Path) -> Optional[pathlib.Path]:
try:
if p.is_file():
bkp = p.with_suffix(p.suffix + f".bak-{int(time.time())}")
bkp.write_text(p.read_text(encoding="utf-8"), encoding="utf-8")
return bkp
except Exception:
pass
return None
nonssl_bkp = backup_if_exists(nonssl_file)
ssl_bkp = backup_if_exists(ssl_file)
# Write new files
try:
nonssl_file.write_text(nonssl_txt, encoding="utf-8")
ssl_file.write_text(ssl_txt, encoding="utf-8")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed writing files: {e}")
# Validate Nginx config (nginx -t)
test = run([SUDO_BIN, NGINX_BIN, "-t"])
if not test["ok"]:
# Restore backups
try:
if nonssl_bkp and nonssl_bkp.exists():
nonssl_file.write_text(nonssl_bkp.read_text(encoding="utf-8"), encoding="utf-8")
if ssl_bkp and ssl_bkp.exists():
ssl_file.write_text(ssl_bkp.read_text(encoding="utf-8"), encoding="utf-8")
except Exception:
pass
raise HTTPException(
status_code=500,
detail=f"nginx -t failed: rc={test['rc']} stderr={test['stderr'] or test['stdout']}",
)
# Reload Nginx (unless skipped)
reload_out: Dict[str, Any] = {"skipped": True}
if not payload.skipReload:
reload_out = run([SUDO_BIN, SYSTEMCTL_BIN, "reload", "nginx"])
if not reload_out["ok"]:
# Config is valid, but reload failed — return error for visibility
raise HTTPException(
status_code=500,
detail=f"Failed to reload nginx: rc={reload_out['rc']} stderr={reload_out['stderr'] or reload_out['stdout']}",
)
return JSONResponse(
{
"ok": True,
"written": [str(nonssl_file), str(ssl_file)],
"test": test,
"reload": reload_out,
"render": {"nonssl": nonssl_txt, "ssl": ssl_txt},
}
)

332
ReverseProxy/fastapi_nginx_gen.py Executable file
View File

@ -0,0 +1,332 @@
#!/usr/bin/env python3
"""
FastAPI-based Nginx reverse-proxy generator.
POST /api/nginx/app
Body:
{
"domain": "ebayapp.data4autos.com",
"port": 3011,
"ip": "147.93.40.215",
"dryRun": false,
"skipReload": false
}
Auth (required unless DISABLE_AUTH=true):
send header: X-API-Key: <API_KEY>
"""
import os
import re
import time
import pathlib
import subprocess
from typing import Optional, Dict, Any
from fastapi import FastAPI, Body, HTTPException, Header, Depends
from fastapi.responses import JSONResponse
from pydantic import BaseModel, field_validator
# ---------------------------- Config ----------------------------
STATIC_IP = os.getenv("STATIC_IP", "147.93.40.215")
PARENT_PATH = os.getenv("PARENT_PATH", "/home/user/conf/web")
PUBLIC_WEB_ROOT = os.getenv("PUBLIC_WEB_ROOT", "/home/user/web")
ERROR_LOG_ROOT = os.getenv("ERROR_LOG_ROOT", "/var/log/apache2/domains")
SUDO_BIN = os.getenv("SUDO_BIN", "/usr/bin/sudo")
NGINX_BIN = os.getenv("NGINX_BIN", "/usr/sbin/nginx")
SYSTEMCTL_BIN = os.getenv("SYSTEMCTL_BIN", "/bin/systemctl")
API_KEY = os.getenv("API_KEY", "")
DISABLE_AUTH = os.getenv("DISABLE_AUTH", "false").lower() in ("1", "true", "yes")
BIND_HOST = os.getenv("BIND_HOST", "127.0.0.1")
BIND_PORT = int(os.getenv("BIND_PORT", "5055"))
# ------------------------- App bootstrap ------------------------
app = FastAPI(title="Nginx Reverse Proxy Generator (FastAPI)")
# ------------------------- Validators ---------------------------
_ipv4_re = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$")
_domain_re = re.compile(r"^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,63}$")
def is_valid_ipv4(ip: str) -> bool:
if not _ipv4_re.match(ip):
return False
parts = ip.split(".")
try:
return all(0 <= int(p) <= 255 and str(int(p)) == p for p in parts)
except Exception:
return False
def is_valid_domain(d: str) -> bool:
return bool(_domain_re.match(d))
def is_valid_port(p: int) -> bool:
return isinstance(p, int) and 1 <= p <= 65535
def safe_domain(d: str) -> str:
if not is_valid_domain(d):
raise ValueError("Invalid domain")
return d.lower()
# --------------------------- Models -----------------------------
class GenRequest(BaseModel):
domain: str
port: int
ip: Optional[str] = None
dryRun: Optional[bool] = False
skipReload: Optional[bool] = False
@field_validator("domain")
@classmethod
def _dom(cls, v: str) -> str:
if not is_valid_domain(v):
raise ValueError("Invalid domain")
return v
@field_validator("port")
@classmethod
def _prt(cls, v: int) -> int:
if not is_valid_port(v):
raise ValueError("Invalid port")
return v
@field_validator("ip")
@classmethod
def _ip(cls, v: Optional[str]) -> Optional[str]:
if v is None or str(v).strip() == "":
return None
if not is_valid_ipv4(v):
raise ValueError("Invalid ip")
return v
# ------------------------- Auth dependency ----------------------
def require_api_key(x_api_key: Optional[str] = Header(default=None)) -> None:
return
if DISABLE_AUTH:
return
if not API_KEY:
raise HTTPException(status_code=500, detail="Server misconfigured: API_KEY not set")
if x_api_key != API_KEY:
print(x_api_key, "!=", API_KEY)
raise HTTPException(status_code=401, detail="Invalid or missing API key")
# ----------------------- Templating funcs -----------------------
def build_ssl(ip: str, domain: str, port: int) -> str:
base = f"{PARENT_PATH}/{domain}"
public = f"{PUBLIC_WEB_ROOT}/{domain}"
return f"""server {{
listen {ip}:443 ssl;
server_name {domain};
error_log {ERROR_LOG_ROOT}/{domain}.error.log error;
ssl_certificate {base}/ssl/{domain}.pem;
ssl_certificate_key {base}/ssl/{domain}.key;
ssl_stapling on;
ssl_stapling_verify on;
# TLS 1.3 0-RTT protection
if ($anti_replay = 307) {{ return 307 https://$host$request_uri; }}
if ($anti_replay = 425) {{ return 425; }}
include {base}/nginx.hsts.conf*;
#################################################################
# FORCE BROWSER TO UPGRADE HTTP REQUESTS TO HTTPS
#################################################################
add_header Content-Security-Policy "upgrade-insecure-requests";
#################################################################
# SECURITY
#################################################################
location ~ /\\.(?!well-known\\/|file) {{
deny all;
return 404;
}}
#################################################################
# MAIN REVERSE PROXY
#################################################################
location / {{
proxy_pass http://127.0.0.1:{port};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
# convert upstream http redirects into https
proxy_redirect http:// https://;
proxy_cache_bypass $http_upgrade;
proxy_request_buffering off;
proxy_connect_timeout 36000s;
proxy_read_timeout 36000s;
proxy_send_timeout 36000s;
send_timeout 36000s;
client_max_body_size 10240m;
}}
#################################################################
location /error/ {{
alias {public}/document_errors/;
}}
disable_symlinks if_not_owner from={public}/public_html;
proxy_hide_header Upgrade;
include {base}/nginx.ssl.conf_*;
}}
""".rstrip() + "\n"
def build_nonssl(ip: str, domain: str, port: int) -> str:
return f"""server {{
listen {ip}:80;
server_name {domain};
return 301 https://$host$request_uri;
}}
""".rstrip() + "\n"
# ----------------------- Shell helpers --------------------------
def run(cmd: list[str], timeout: int = 30) -> dict:
try:
p = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout,
check=False,
text=True,
)
return {"ok": p.returncode == 0, "rc": p.returncode, "stdout": p.stdout, "stderr": p.stderr}
except subprocess.TimeoutExpired as te:
return {"ok": False, "rc": -1, "stdout": te.stdout or "", "stderr": f"Timeout: {te}"}
except Exception as e:
return {"ok": False, "rc": -2, "stdout": "", "stderr": str(e)}
# --------------------------- Routes -----------------------------
@app.get("/healthz")
def healthz():
return {"ok": True, "ip": STATIC_IP, "parent": PARENT_PATH}
@app.post("/api/nginx/app", dependencies=[Depends(require_api_key)])
def create_or_update_vhost(payload: GenRequest = Body(...)) -> JSONResponse:
ip = payload.ip or STATIC_IP
domain = safe_domain(payload.domain)
port = int(payload.port)
ssl_txt = build_ssl(ip, domain, port)
nonssl_txt = build_nonssl(ip, domain, port)
domain_dir = pathlib.Path(PARENT_PATH) / domain
ssl_file = domain_dir / "nginx.ssl.conf"
nonssl_file = domain_dir / "nginx.conf"
if payload.dryRun:
return JSONResponse(
{
"ok": True,
"dryRun": True,
"files": {
str(nonssl_file): nonssl_txt,
str(ssl_file): ssl_txt,
},
}
)
try:
domain_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to create {domain_dir}: {e}")
if not os.access(domain_dir, os.W_OK | os.X_OK):
raise HTTPException(
status_code=403,
detail=f"No write access to {domain_dir}. Run as root or fix permissions.",
)
def backup_if_exists(p: pathlib.Path) -> Optional[pathlib.Path]:
try:
if p.is_file():
bkp = p.with_suffix(p.suffix + f".bak-{int(time.time())}")
bkp.write_text(p.read_text(encoding="utf-8"), encoding="utf-8")
return bkp
except Exception:
pass
return None
nonssl_bkp = backup_if_exists(nonssl_file)
ssl_bkp = backup_if_exists(ssl_file)
try:
nonssl_file.write_text(nonssl_txt, encoding="utf-8")
ssl_file.write_text(ssl_txt, encoding="utf-8")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed writing files: {e}")
test = run([SUDO_BIN, NGINX_BIN, "-t"])
if not test["ok"]:
try:
if nonssl_bkp and nonssl_bkp.exists():
nonssl_file.write_text(nonssl_bkp.read_text(encoding="utf-8"), encoding="utf-8")
if ssl_bkp and ssl_bkp.exists():
ssl_file.write_text(ssl_bkp.read_text(encoding="utf-8"), encoding="utf-8")
except Exception:
pass
raise HTTPException(
status_code=500,
detail=f"nginx -t failed: rc={test['rc']} stderr={test['stderr'] or test['stdout']}",
)
reload_out: Dict[str, Any] = {"skipped": True}
if not payload.skipReload:
reload_out = run([SUDO_BIN, SYSTEMCTL_BIN, "reload", "nginx"])
if not reload_out["ok"]:
raise HTTPException(
status_code=500,
detail=f"Failed to reload nginx: rc={reload_out['rc']} stderr={reload_out['stderr'] or reload_out['stdout']}",
)
return JSONResponse(
{
"ok": True,
"written": [str(nonssl_file), str(ssl_file)],
"test": test,
"reload": reload_out,
"render": {"nonssl": nonssl_txt, "ssl": ssl_txt},
}
)

3
ReverseProxy/requirements.txt Executable file
View File

@ -0,0 +1,3 @@
fastapi==0.115.0
uvicorn[standard]==0.30.6
pydantic==2.9.2

926
package-lock.json generated Executable file
View File

@ -0,0 +1,926 @@
{
"name": "management",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "management",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"dotenv": "^17.2.2",
"express": "^5.1.0",
"morgan": "^1.10.1"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/basic-auth/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.0",
"http-errors": "^2.0.0",
"iconv-lite": "^0.6.3",
"on-finished": "^2.4.1",
"qs": "^6.14.0",
"raw-body": "^3.0.0",
"type-is": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dotenv": {
"version": "17.2.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
"integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.0",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"vary": "^1.1.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/http-errors/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"license": "MIT",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.1.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/morgan/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/morgan/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/morgan/node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/on-headers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.7.0",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
"is-promise": "^4.0.0",
"parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.5",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"mime-types": "^3.0.1",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}
}

19
package.json Executable file
View File

@ -0,0 +1,19 @@
{
"name": "management",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"dotenv": "^17.2.2",
"express": "^5.1.0",
"morgan": "^1.10.1"
},
"description": ""
}

292
routes/nginx.js Executable file
View File

@ -0,0 +1,292 @@
// routes/nginx.js
import { Router } from "express";
import { promises as fs } from "fs";
import path from "node:path";
import { execFile } from "node:child_process";
import {
STATIC_IP,
PARENT_PATH,
PUBLIC_WEB_ROOT,
ERROR_LOG_ROOT,
} from "../server.js";
const router = Router();
// ---- absolute binaries (systemd PATH is tiny) ----
const SUDO_BIN = process.env.SUDO_BIN || "/usr/bin/sudo";
const NGINX_BIN = process.env.NGINX_BIN || "/usr/sbin/nginx";
const SYSTEMCTL_BIN = process.env.SYSTEMCTL_BIN || "/bin/systemctl";
// ---------- helpers ----------
const isValidIPv4 = (ip) =>
/^(\d{1,3}\.){3}\d{1,3}$/.test(ip) &&
ip.split(".").every((n) => {
const v = Number(n);
return v >= 0 && v <= 255 && String(v) === n;
});
const isValidDomain = (d) =>
/^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,63}$/.test(d);
const isValidPort = (p) =>
Number.isInteger(Number(p)) && Number(p) >= 1 && Number(p) <= 65535;
// very small sanitizer to avoid path traversal via domain
function safeDomain(d) {
if (!isValidDomain(d)) throw new Error("Invalid domain");
return d.toLowerCase();
}
function buildSSL({ ip, domain, port }) {
const basePath = path.posix.join(PARENT_PATH, domain);
const publicPath = path.posix.join(PUBLIC_WEB_ROOT, domain);
return `server {
listen ${ip}:443 ssl;
server_name ${domain};
error_log ${path.posix.join(ERROR_LOG_ROOT, `${domain}.error.log`)} error;
ssl_certificate ${path.posix.join(basePath, "ssl", `${domain}.pem`)};
ssl_certificate_key ${path.posix.join(basePath, "ssl", `${domain}.key`)};
location / {
proxy_pass http://127.0.0.1:${port};
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;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ /\\.(?!well-known\\/|file) {
deny all;
return 404;
}
location /error/ {
alias ${path.posix.join(publicPath, "document_errors")}/;
}
disable_symlinks if_not_owner from=${path.posix.join(publicPath, "public_html")};
include ${path.posix.join(basePath, "nginx.ssl.conf_*")};
}`;
}
function buildNonSSL({ ip, domain, port }) {
const basePath = path.posix.join(PARENT_PATH, domain);
const publicPath = path.posix.join(PUBLIC_WEB_ROOT, domain);
return `server {
listen ${ip}:80;
server_name ${domain};
error_log ${path.posix.join(ERROR_LOG_ROOT, `${domain}.error.log`)} error;
include ${path.posix.join(basePath, "nginx.forcessl.conf*")};
location / {
proxy_pass http://127.0.0.1:${port};
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;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ /\\.(?!well-known\\/|file) {
deny all;
return 404;
}
location /error/ {
alias ${path.posix.join(publicPath, "document_errors")}/;
}
disable_symlinks if_not_owner from=${path.posix.join(publicPath, "public_html")};
include ${path.posix.join(basePath, "nginx.conf_*")};
}`;
}
function sh(cmd, args = [], opts = {}) {
return new Promise((resolve, reject) => {
execFile(cmd, args, { timeout: 30_000, ...opts }, (err, stdout, stderr) => {
if (err) return reject({ err, stdout, stderr });
resolve({ stdout, stderr });
});
});
}
async function exists(p) {
try {
await fs.stat(p);
return true;
} catch {
return false;
}
}
// ---------- routes ----------
/**
* POST /api/nginx/app
* body: { domain: string, port: number|string, ip?: string, dryRun?: boolean, skipReload?: boolean }
*/
router.post("/nginx/app", async (req, res) => {
try {
const { domain, port, ip: ipIn, dryRun = false, skipReload = false } = req.body || {};
if (!domain || !port) {
return res.status(400).json({ ok: false, error: "domain and port are required" });
}
const ip = ipIn && String(ipIn).trim() ? String(ipIn).trim() : STATIC_IP;
if (!isValidDomain(domain)) {
return res.status(400).json({ ok: false, error: "Invalid domain" });
}
if (!isValidPort(port)) {
return res.status(400).json({ ok: false, error: "Invalid port" });
}
if (!isValidIPv4(ip)) {
return res.status(400).json({ ok: false, error: "Invalid ip" });
}
const dom = safeDomain(domain);
const context = { ip, domain: dom, port: Number(port) };
const sslConf = buildSSL(context);
const nonsslConf = buildNonSSL(context);
const domainDir = path.join(PARENT_PATH, dom);
const sslFile = path.join(domainDir, "nginx.ssl.conf");
const nonsslFile = path.join(domainDir, "nginx.conf");
if (dryRun) {
return res.json({
ok: true,
dryRun: true,
files: {
[nonsslFile]: nonsslConf,
[sslFile]: sslConf,
},
});
}
// ensure directory exists
await fs.mkdir(domainDir, { recursive: true });
// preflight: can we write to the dir?
try {
await fs.access(domainDir, fs.constants.W_OK | fs.constants.X_OK);
} catch {
return res.status(403).json({
ok: false,
error: `No write access to ${domainDir} for this process. Fix permissions or run with sudo.`,
});
}
// backup previous if exist (returns backup path or null)
async function backupIfExists(fpath) {
try {
const stat = await fs.stat(fpath);
if (stat.isFile()) {
const bkp = `${fpath}.bak-${Date.now()}`;
await fs.copyFile(fpath, bkp);
return bkp;
}
} catch {}
return null;
}
const nonsslBkp = await backupIfExists(nonsslFile);
const sslBkp = await backupIfExists(sslFile);
// write new files
await fs.writeFile(nonsslFile, nonsslConf, "utf8");
await fs.writeFile(sslFile, sslConf, "utf8");
// nginx -t (as root, via sudo, with absolute paths)
let testOut;
try {
//testOut = await sh(SUDO_BIN, [NGINX_BIN, "-t"]);
} catch (e) {
// restore backups if test fails
try {
if (nonsslBkp && (await exists(nonsslBkp))) await fs.copyFile(nonsslBkp, nonsslFile);
if (sslBkp && (await exists(sslBkp))) await fs.copyFile(sslBkp, sslFile);
} catch {}
return res.status(500).json({
ok: false,
error: "nginx -t failed",
details: e.stderr || e.stdout || String(e.err),
});
}
// // reload (always via sudo/systemctl)
// let reloadOut = { stdout: "", stderr: "" };
// if (!skipReload) {
// try {
// reloadOut = await sh(SUDO_BIN, [SYSTEMCTL_BIN, "reload", "nginx"]);
// } catch (e) {
// return res.status(500).json({
// ok: false,
// error: "Failed to reload nginx",
// details: e.stderr || e.stdout || String(e.err),
// test: testOut,
// });
// }
// }
// reload (always via sudo)
let reloadOut = { stdout: "", stderr: "" };
if (!skipReload) {
try {
// 1) Test nginx configuration
const testOut = await sh("sudo", ["nginx", "-t"]);
if (testOut.stderr?.length) {
// nginx -t writes to stderr even on success, so check exit code if sh() gives it
console.log("nginx -t:", testOut.stderr);
}
// 2) Reload nginx if the test passed
reloadOut = await sh("sudo", ["systemctl", "reload", "nginx"]);
return res.json({
ok: true,
test: testOut,
reload: reloadOut,
});
} catch (e) {
return res.status(500).json({
ok: false,
error: "Failed to validate or reload nginx",
details: e.stderr || e.stdout || String(e.err),
});
}
}
return res.json({
ok: true,
written: [nonsslFile, sslFile],
tested: testOut,
reloaded: skipReload ? "skipped" : reloadOut,
render: { nonssl: nonsslConf, ssl: sslConf },
});
} catch (err) {
return res.status(500).json({ ok: false, error: String(err && err.message ? err.message : err) });
}
});
export default router;

41
server.js Executable file
View File

@ -0,0 +1,41 @@
// server.js
// Express app bootstrap for Nginx vhost generator/updater
// server.js
import express from "express";
import morgan from "morgan";
import nginxRouter from "./routes/nginx.js";
import dotenv from "dotenv";
dotenv.config(); // loads .env if present
// ---- Static defaults (env first, fallback second) ----
export const STATIC_IP = process.env.STATIC_IP || "147.93.40.215";
export const PARENT_PATH = process.env.PARENT_PATH || "/home/user/conf/web"; // <parent>/<domain>/
export const PUBLIC_WEB_ROOT = process.env.PUBLIC_WEB_ROOT || "/home/user/web"; // for /error alias
export const ERROR_LOG_ROOT = process.env.ERROR_LOG_ROOT || "/var/log/apache2/domains";
// How to restart Nginx (can be "reload" or full "restart")
export const NGINX_RELOAD_CMD = process.env.NGINX_RELOAD_CMD || "systemctl reload nginx";
// -----------------------------------------------------
const app = express();
app.use(express.json({ limit: "256kb" }));
app.use(morgan("tiny"));
// health
app.get("/healthz", (_req, res) => res.json({ ok: true }));
// api
app.use("/api", nginxRouter);
// 404
app.use((_req, res) => res.status(404).json({ ok: false, error: "Not found" }));
const PORT = process.env.PORT || 5055;
app.listen(PORT, () => {
console.log(`Nginx generator API listening on port ${PORT}`);
console.log(`Defaults -> IP: ${STATIC_IP}, PARENT_PATH: ${PARENT_PATH}`);
});