From 65e74c68328081b89628a15bfdcce6a225cf610e Mon Sep 17 00:00:00 2001 From: nessi Date: Tue, 17 Mar 2026 22:09:37 +0100 Subject: [PATCH] feat: add public-facing web interface with domain-based routing Add public-web service with static landing page for client enrollment and device provisioning. Add public-web container to docker-compose with port 8082. Configure nginx reverse proxy with domain-based routing: admin-vpn.nesterovic.cc for admin interface and vpn.nesterovic.cc for public interface. Add proxy headers for X-Real-IP, X-Forwarded-For and X-Forwarded-Proto to both server blocks. Create public-web Dockerfile with nginx serving --- deploy/docker-compose.yml | 10 +++ deploy/nginx/reverse-proxy.conf | 25 ++++++- public-web/Dockerfile | 8 ++ public-web/index.html | 35 +++++++++ public-web/nginx.conf | 10 +++ public-web/styles.css | 126 ++++++++++++++++++++++++++++++++ 6 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 public-web/Dockerfile create mode 100644 public-web/index.html create mode 100644 public-web/nginx.conf create mode 100644 public-web/styles.css diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index e012998..3e92d0c 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -44,11 +44,21 @@ services: networks: - control + public-web: + build: + context: .. + dockerfile: public-web/Dockerfile + ports: + - "8082:80" + networks: + - control + reverse-proxy: image: nginx:1.27-alpine depends_on: - backend - admin-web + - public-web ports: - "80:80" volumes: diff --git a/deploy/nginx/reverse-proxy.conf b/deploy/nginx/reverse-proxy.conf index 70a7216..c960149 100644 --- a/deploy/nginx/reverse-proxy.conf +++ b/deploy/nginx/reverse-proxy.conf @@ -1,6 +1,6 @@ server { listen 80; - server_name _; + server_name admin-vpn.nesterovic.cc; location /api/ { proxy_pass http://backend:8080; @@ -13,6 +13,29 @@ server { location / { proxy_pass http://admin-web:80; 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-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name vpn.nesterovic.cc _; + + location /api/ { + proxy_pass http://backend:8080; + 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-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://public-web:80; + 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-Forwarded-Proto $scheme; } } diff --git a/public-web/Dockerfile b/public-web/Dockerfile new file mode 100644 index 0000000..1cddda0 --- /dev/null +++ b/public-web/Dockerfile @@ -0,0 +1,8 @@ +FROM nginx:1.27-alpine + +COPY public-web/index.html /usr/share/nginx/html/index.html +COPY public-web/styles.css /usr/share/nginx/html/styles.css +COPY admin-web/public/NexaVPN_Logo.png /usr/share/nginx/html/NexaVPN_Logo.png +COPY admin-web/public/NexaVPN_Logo_Only.png /usr/share/nginx/html/NexaVPN_Logo_Only.png +COPY admin-web/public/favicon.ico /usr/share/nginx/html/favicon.ico +COPY public-web/nginx.conf /etc/nginx/conf.d/default.conf diff --git a/public-web/index.html b/public-web/index.html new file mode 100644 index 0000000..e2c8532 --- /dev/null +++ b/public-web/index.html @@ -0,0 +1,35 @@ + + + + + + NexaVPN + + + + +
+
+ +

Private access

+

Connect with the NexaVPN app.

+

+ Use the desktop client to sign in, provision this device, and connect to your private network. +

+ +
+ +
+

What this host is for

+
    +
  • Desktop client login and device enrollment
  • +
  • Profile sync for provisioned devices
  • +
  • Public VPN entrypoint information
  • +
+
+
+ + diff --git a/public-web/nginx.conf b/public-web/nginx.conf new file mode 100644 index 0000000..9a7833a --- /dev/null +++ b/public-web/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri /index.html; + } +} diff --git a/public-web/styles.css b/public-web/styles.css new file mode 100644 index 0000000..4e8c343 --- /dev/null +++ b/public-web/styles.css @@ -0,0 +1,126 @@ +:root { + color-scheme: dark; + font-family: "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif; + --bg: #08111d; + --panel: rgba(15, 24, 41, 0.82); + --text: #eef4ff; + --muted: #a7b7d4; + --line: rgba(177, 197, 229, 0.14); + --accent: #74e0b8; + --accent-strong: #1fb67a; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + color: var(--text); + background: + radial-gradient(circle at top left, rgba(116, 224, 184, 0.17), transparent 25%), + radial-gradient(circle at right, rgba(74, 120, 255, 0.12), transparent 20%), + linear-gradient(180deg, #07101c 0%, #0d1728 100%); +} + +.shell { + width: min(960px, 100%); + margin: 0 auto; + min-height: 100vh; + display: grid; + align-content: center; + gap: 24px; + padding: 32px 24px; +} + +.hero, +.card { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 28px; + box-shadow: 0 24px 72px rgba(3, 8, 20, 0.34); + backdrop-filter: blur(16px); +} + +.hero { + display: grid; + gap: 18px; + padding: 32px; +} + +.logo { + width: min(280px, 100%); + height: auto; +} + +.eyebrow { + margin: 0; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.18em; + font-size: 0.78rem; +} + +h1, +h2, +p, +ul { + margin: 0; +} + +h1 { + font-size: clamp(2rem, 5vw, 3.4rem); + line-height: 1.05; +} + +.copy { + max-width: 42rem; + color: var(--muted); + line-height: 1.6; + font-size: 1.05rem; +} + +.actions { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 18px; + border-radius: 999px; + text-decoration: none; + font-weight: 700; +} + +.button.primary { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); + color: #04141a; +} + +.button.secondary { + border: 1px solid var(--line); + color: var(--text); + background: rgba(255, 255, 255, 0.03); +} + +.card { + padding: 24px 28px; +} + +.card ul { + padding-left: 20px; + color: var(--muted); + line-height: 1.8; +} + +@media (max-width: 720px) { + .hero, + .card { + padding: 24px; + } +}