webgate 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {webgate-0.2.1 → webgate-0.2.2}/CHANGELOG.md +17 -0
  2. webgate-0.2.2/Dockerfile.demo +63 -0
  3. {webgate-0.2.1 → webgate-0.2.2}/PKG-INFO +4 -1
  4. {webgate-0.2.1 → webgate-0.2.2}/README.md +3 -0
  5. webgate-0.2.2/fly.toml +48 -0
  6. {webgate-0.2.1 → webgate-0.2.2}/pyproject.toml +1 -1
  7. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/app.py +31 -1
  8. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/config.py +1 -0
  9. webgate-0.2.2/src/webgate/demo.py +55 -0
  10. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/static/index.html +11 -0
  11. {webgate-0.2.1 → webgate-0.2.2}/.dockerignore +0 -0
  12. {webgate-0.2.1 → webgate-0.2.2}/.github/workflows/docs.yml +0 -0
  13. {webgate-0.2.1 → webgate-0.2.2}/.gitignore +0 -0
  14. {webgate-0.2.1 → webgate-0.2.2}/Dockerfile +0 -0
  15. {webgate-0.2.1 → webgate-0.2.2}/Dockerfile.ssh-demo +0 -0
  16. {webgate-0.2.1 → webgate-0.2.2}/LICENSE +0 -0
  17. {webgate-0.2.1 → webgate-0.2.2}/ROADMAP.md +0 -0
  18. {webgate-0.2.1 → webgate-0.2.2}/VERSION +0 -0
  19. {webgate-0.2.1 → webgate-0.2.2}/compose.dev.yml +0 -0
  20. {webgate-0.2.1 → webgate-0.2.2}/compose.yml +0 -0
  21. {webgate-0.2.1 → webgate-0.2.2}/docs/api/auth.md +0 -0
  22. {webgate-0.2.1 → webgate-0.2.2}/docs/api/files.md +0 -0
  23. {webgate-0.2.1 → webgate-0.2.2}/docs/api/servers.md +0 -0
  24. {webgate-0.2.1 → webgate-0.2.2}/docs/api/terminal.md +0 -0
  25. {webgate-0.2.1 → webgate-0.2.2}/docs/changelog.md +0 -0
  26. {webgate-0.2.1 → webgate-0.2.2}/docs/getting-started/installation.md +0 -0
  27. {webgate-0.2.1 → webgate-0.2.2}/docs/getting-started/quickstart.md +0 -0
  28. {webgate-0.2.1 → webgate-0.2.2}/docs/guide/files.md +0 -0
  29. {webgate-0.2.1 → webgate-0.2.2}/docs/guide/servers.md +0 -0
  30. {webgate-0.2.1 → webgate-0.2.2}/docs/guide/split.md +0 -0
  31. {webgate-0.2.1 → webgate-0.2.2}/docs/guide/terminal.md +0 -0
  32. {webgate-0.2.1 → webgate-0.2.2}/docs/guide/users.md +0 -0
  33. {webgate-0.2.1 → webgate-0.2.2}/docs/index.md +0 -0
  34. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/access-control.png +0 -0
  35. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/audit.png +0 -0
  36. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/edit-access-control.png +0 -0
  37. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/editor.png +0 -0
  38. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/light-theme.png +0 -0
  39. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/login.png +0 -0
  40. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/new-server-form.png +0 -0
  41. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/sftp-restricted.png +0 -0
  42. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/sftp.png +0 -0
  43. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/site-manager.png +0 -0
  44. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/split-view.png +0 -0
  45. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/ssh-disabled.png +0 -0
  46. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/terminal.png +0 -0
  47. {webgate-0.2.1 → webgate-0.2.2}/docs/screenshots/users.png +0 -0
  48. {webgate-0.2.1 → webgate-0.2.2}/mkdocs.yml +0 -0
  49. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/__init__.py +0 -0
  50. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/__main__.py +0 -0
  51. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/audit/__init__.py +0 -0
  52. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/audit/models.py +0 -0
  53. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/audit/service.py +0 -0
  54. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/auth/__init__.py +0 -0
  55. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/auth/models.py +0 -0
  56. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/auth/routes.py +0 -0
  57. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/auth/service.py +0 -0
  58. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/db/__init__.py +0 -0
  59. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/db/engine.py +0 -0
  60. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/files/__init__.py +0 -0
  61. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/files/models.py +0 -0
  62. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/files/pool.py +0 -0
  63. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/files/routes.py +0 -0
  64. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/files/sftp_service.py +0 -0
  65. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/servers/__init__.py +0 -0
  66. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/servers/crypto.py +0 -0
  67. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/servers/models.py +0 -0
  68. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/servers/monitor.py +0 -0
  69. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/servers/routes.py +0 -0
  70. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/servers/service.py +0 -0
  71. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/terminal/__init__.py +0 -0
  72. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/terminal/routes.py +0 -0
  73. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/terminal/ssh_session.py +0 -0
  74. {webgate-0.2.1 → webgate-0.2.2}/src/webgate/terminal/ws_handler.py +0 -0
  75. {webgate-0.2.1 → webgate-0.2.2}/tests/__init__.py +0 -0
  76. {webgate-0.2.1 → webgate-0.2.2}/tests/conftest.py +0 -0
  77. {webgate-0.2.1 → webgate-0.2.2}/tests/test_auth.py +0 -0
  78. {webgate-0.2.1 → webgate-0.2.2}/tests/test_files.py +0 -0
  79. {webgate-0.2.1 → webgate-0.2.2}/tests/test_servers.py +0 -0
  80. {webgate-0.2.1 → webgate-0.2.2}/tests/test_terminal.py +0 -0
  81. {webgate-0.2.1 → webgate-0.2.2}/uv.lock +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.2.2 (2026-04-15)
4
+
5
+ ### Features
6
+
7
+ - **Demo mode** (`WEBGATE_DEMO_MODE=true`) -- read-only public demo deployments. Blocks every write request on `/api/*` (except login), disables the WebSocket quick-connect endpoint, seeds a `demo`/`demo` user with a sample server, and shows a top banner in the UI.
8
+ - **`Dockerfile.demo`** -- single-container image bundling webgate + a sandboxed `sshd` target via supervisord. Ready for free hosting tiers.
9
+ - **`fly.toml`** -- Fly.io configuration for one-command demo deployments (`flyctl deploy`).
10
+
11
+ ### Details
12
+
13
+ - New public endpoint `GET /api/config` exposes `{"demo_mode": bool}` for the frontend to render the banner before login
14
+ - `WEBGATE_DEMO_MODE=true` adds an HTTP middleware that returns `403` for any `POST/PUT/PATCH/DELETE` on `/api/*` (allowlist: `/api/auth/login`, `/api/auth/totp/verify`)
15
+ - Demo seed (`webgate.demo`) is idempotent and only runs when the flag is on
16
+ - Hourly state reset for the public demo can be done with a cron pinging the container restart, so DB returns to seed state
17
+
18
+ ---
19
+
3
20
  ## v0.2.1 (2026-04-15)
4
21
 
5
22
  ### Features
@@ -0,0 +1,63 @@
1
+ # Single-container demo image: webgate + a sandboxed SSH/SFTP target
2
+ # managed by supervisord. Suitable for free tiers (Fly.io, Render) where
3
+ # spinning a sidecar VM isn't ideal.
4
+ #
5
+ # Build: docker build -f Dockerfile.demo -t webgate-demo .
6
+ # Run: docker run -p 8443:8443 -e WEBGATE_SECRET_KEY=demo webgate-demo
7
+
8
+ # --- Stage 1: build webgate ---
9
+ FROM python:3.13-slim AS builder
10
+ WORKDIR /app
11
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
12
+ COPY pyproject.toml uv.lock README.md ./
13
+ RUN uv sync --frozen --no-dev --no-editable
14
+ COPY src/ src/
15
+
16
+ # --- Stage 2: runtime with webgate + sshd + supervisord ---
17
+ FROM python:3.13-slim
18
+
19
+ RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends \
21
+ openssh-server supervisor procps curl nano htop tree && \
22
+ rm -rf /var/lib/apt/lists/*
23
+
24
+ # sshd config: only the demo user, password auth allowed for the demo
25
+ RUN mkdir -p /run/sshd && \
26
+ sed -i 's/^#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config && \
27
+ sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config && \
28
+ useradd -m -s /bin/bash demo && \
29
+ echo 'demo:demo' | chpasswd
30
+
31
+ # Sample tree so SFTP browsing has something to show
32
+ RUN mkdir -p /home/demo/projects/webgate /home/demo/projects/api-server \
33
+ /home/demo/documents /home/demo/logs /home/demo/scripts && \
34
+ printf '# webgate\nSelf-hosted SSH + SFTP manager\n' > /home/demo/projects/webgate/README.md && \
35
+ printf '{"name":"api-server","version":"2.1.0"}\n' > /home/demo/projects/api-server/package.json && \
36
+ printf 'Hello from the webgate demo!\n' > /home/demo/documents/welcome.txt && \
37
+ printf '2026-04-08 10:00:00 INFO Server started on port 8080\n' > /home/demo/logs/app.log && \
38
+ printf '#!/bin/bash\necho "Deploy complete"\n' > /home/demo/scripts/deploy.sh && \
39
+ chmod +x /home/demo/scripts/deploy.sh && \
40
+ chown -R demo:demo /home/demo
41
+
42
+ # webgate
43
+ WORKDIR /app
44
+ COPY --from=builder /app/.venv /app/.venv
45
+ COPY --from=builder /app/src /app/src
46
+ RUN mkdir -p /data
47
+ ENV PATH="/app/.venv/bin:$PATH" \
48
+ PYTHONPATH="/app/src" \
49
+ WEBGATE_HOST=0.0.0.0 \
50
+ WEBGATE_PORT=8443 \
51
+ WEBGATE_DB_URL=sqlite+aiosqlite:////data/webgate.db \
52
+ WEBGATE_LOG_LEVEL=info \
53
+ WEBGATE_DEMO_MODE=true
54
+
55
+ # supervisord config
56
+ RUN printf '[supervisord]\nnodaemon=true\nuser=root\nlogfile=/dev/stdout\nlogfile_maxbytes=0\n\n[program:sshd]\ncommand=/usr/sbin/sshd -D -e\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:webgate]\ncommand=python -m webgate\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n' > /etc/supervisor/conf.d/webgate.conf
57
+
58
+ EXPOSE 8443
59
+
60
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
61
+ CMD curl -fsS http://localhost:8443/api/health || exit 1
62
+
63
+ CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/webgate.conf"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webgate
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Self-hosted web application for remote server management via SSH terminal and SFTP file browser
5
5
  Project-URL: Homepage, https://github.com/kalexnolasco/webgate
6
6
  Project-URL: Documentation, https://kalexnolasco.github.io/webgate/
@@ -58,6 +58,8 @@ Description-Content-Type: text/markdown
58
58
  Self-hosted web application for remote server management via **SSH terminal** and **SFTP file browser**. A modern Python replacement combining the best of [webssh](https://github.com/huashengdun/webssh) and [filebrowser](https://github.com/filebrowser/filebrowser) into a single unified tool with a FileZilla-inspired interface.
59
59
 
60
60
  > **Documentation: [kalexnolasco.github.io/webgate](https://kalexnolasco.github.io/webgate/)**
61
+ >
62
+ > **🎮 Live demo: [webgate-demo.fly.dev](https://webgate-demo.fly.dev/)** — login `demo` / `demo` (read-only, resets hourly)
61
63
 
62
64
  ---
63
65
 
@@ -449,6 +451,7 @@ All settings are configurable via environment variables with the `WEBGATE_` pref
449
451
  | `WEBGATE_DB_URL` | `sqlite+aiosqlite:///./webgate.db` | Database URL |
450
452
  | `WEBGATE_ALLOWED_ORIGINS` | `*` | CORS origins (comma-separated) |
451
453
  | `WEBGATE_ROOT_PATH` | `` (empty) | URL prefix when served behind a reverse proxy at a sub-path (e.g. `/webgate`) |
454
+ | `WEBGATE_DEMO_MODE` | `false` | Read-only public demo: blocks all writes, seeds a `demo`/`demo` user and shows a banner |
452
455
  | `WEBGATE_LOG_LEVEL` | `info` | Log level |
453
456
  | `WEBGATE_SESSION_TIMEOUT` | `3600` | SSH session timeout (seconds) |
454
457
  | `WEBGATE_MAX_UPLOAD_SIZE` | `104857600` | Max upload size (100 MB) |
@@ -12,6 +12,8 @@
12
12
  Self-hosted web application for remote server management via **SSH terminal** and **SFTP file browser**. A modern Python replacement combining the best of [webssh](https://github.com/huashengdun/webssh) and [filebrowser](https://github.com/filebrowser/filebrowser) into a single unified tool with a FileZilla-inspired interface.
13
13
 
14
14
  > **Documentation: [kalexnolasco.github.io/webgate](https://kalexnolasco.github.io/webgate/)**
15
+ >
16
+ > **🎮 Live demo: [webgate-demo.fly.dev](https://webgate-demo.fly.dev/)** — login `demo` / `demo` (read-only, resets hourly)
15
17
 
16
18
  ---
17
19
 
@@ -403,6 +405,7 @@ All settings are configurable via environment variables with the `WEBGATE_` pref
403
405
  | `WEBGATE_DB_URL` | `sqlite+aiosqlite:///./webgate.db` | Database URL |
404
406
  | `WEBGATE_ALLOWED_ORIGINS` | `*` | CORS origins (comma-separated) |
405
407
  | `WEBGATE_ROOT_PATH` | `` (empty) | URL prefix when served behind a reverse proxy at a sub-path (e.g. `/webgate`) |
408
+ | `WEBGATE_DEMO_MODE` | `false` | Read-only public demo: blocks all writes, seeds a `demo`/`demo` user and shows a banner |
406
409
  | `WEBGATE_LOG_LEVEL` | `info` | Log level |
407
410
  | `WEBGATE_SESSION_TIMEOUT` | `3600` | SSH session timeout (seconds) |
408
411
  | `WEBGATE_MAX_UPLOAD_SIZE` | `104857600` | Max upload size (100 MB) |
webgate-0.2.2/fly.toml ADDED
@@ -0,0 +1,48 @@
1
+ # Fly.io deployment for the public webgate demo.
2
+ # Edit `app` to your fly app name, then:
3
+ # flyctl launch --no-deploy --copy-config
4
+ # flyctl secrets set WEBGATE_SECRET_KEY=$(openssl rand -hex 32)
5
+ # flyctl deploy
6
+ #
7
+ # Free tier (1x shared-cpu, 256-512 MB) is enough for light demo traffic.
8
+
9
+ app = "webgate-demo"
10
+ primary_region = "cdg" # Paris; closest EU region to Spain
11
+
12
+ [build]
13
+ dockerfile = "Dockerfile.demo"
14
+
15
+ [env]
16
+ WEBGATE_DEMO_MODE = "true"
17
+ WEBGATE_LOG_LEVEL = "info"
18
+
19
+ [http_service]
20
+ internal_port = 8443
21
+ force_https = true
22
+ auto_stop_machines = "stop" # save free-tier hours when idle
23
+ auto_start_machines = true
24
+ min_machines_running = 0
25
+
26
+ [http_service.concurrency]
27
+ type = "requests"
28
+ soft_limit = 50
29
+ hard_limit = 100
30
+
31
+ [[http_service.checks]]
32
+ interval = "30s"
33
+ timeout = "5s"
34
+ grace_period = "15s"
35
+ method = "get"
36
+ path = "/api/health"
37
+
38
+ [[vm]]
39
+ cpu_kind = "shared"
40
+ cpus = 1
41
+ memory_mb = 512
42
+
43
+ # Persistent volume so the demo seed survives restarts. The hourly reset
44
+ # (see scripts/demo-reset.sh) wipes the DB inside the container, not the
45
+ # whole volume, so this stays cheap.
46
+ [mounts]
47
+ source = "webgate_demo_data"
48
+ destination = "/data"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webgate"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Self-hosted web application for remote server management via SSH terminal and SFTP file browser"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,8 +1,9 @@
1
1
  from collections.abc import AsyncGenerator
2
2
  from contextlib import asynccontextmanager
3
3
 
4
- from fastapi import FastAPI
4
+ from fastapi import FastAPI, Request
5
5
  from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import JSONResponse
6
7
  from fastapi.staticfiles import StaticFiles
7
8
  from slowapi import _rate_limit_exceeded_handler
8
9
  from slowapi.errors import RateLimitExceeded
@@ -12,6 +13,7 @@ from webgate.auth.routes import router as auth_router
12
13
  from webgate.auth.service import seed_admin
13
14
  from webgate.config import settings
14
15
  from webgate.db.engine import async_session_factory, close_db, init_db
16
+ from webgate.demo import seed_demo
15
17
  from webgate.files.pool import sftp_pool
16
18
  from webgate.files.routes import router as files_router
17
19
  from webgate.servers.monitor import server_monitor
@@ -24,6 +26,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
24
26
  await init_db()
25
27
  async with async_session_factory() as session:
26
28
  await seed_admin(session)
29
+ if settings.demo_mode:
30
+ await seed_demo(session)
27
31
  await sftp_pool.start()
28
32
  await server_monitor.start()
29
33
  yield
@@ -55,6 +59,32 @@ def create_app() -> FastAPI:
55
59
  async def health() -> dict[str, str]: # pyright: ignore[reportUnusedFunction]
56
60
  return {"status": "ok"}
57
61
 
62
+ @app.get("/api/config")
63
+ async def public_config() -> dict[str, object]: # pyright: ignore[reportUnusedFunction]
64
+ # Public flags consumed by the frontend before login.
65
+ return {"demo_mode": settings.demo_mode}
66
+
67
+ if settings.demo_mode:
68
+ # In demo mode block any state-changing request on /api/* except login
69
+ # and the WS quick-connect endpoint (no arbitrary SSH targets allowed).
70
+ WRITE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
71
+ WRITE_ALLOWLIST = {"/api/auth/login", "/api/auth/totp/verify"}
72
+
73
+ @app.middleware("http")
74
+ async def _demo_readonly(request: Request, call_next): # pyright: ignore[reportUnusedFunction]
75
+ path = request.url.path
76
+ if request.method in WRITE_METHODS and path.startswith("/api/") and path not in WRITE_ALLOWLIST:
77
+ return JSONResponse(
78
+ status_code=403,
79
+ content={"detail": "Demo mode: write operations are disabled"},
80
+ )
81
+ if path.endswith("/api/ws/terminal/quick"):
82
+ return JSONResponse(
83
+ status_code=403,
84
+ content={"detail": "Demo mode: quick-connect is disabled"},
85
+ )
86
+ return await call_next(request)
87
+
58
88
  app.include_router(auth_router)
59
89
  app.include_router(servers_router)
60
90
  app.include_router(terminal_router)
@@ -12,6 +12,7 @@ class Settings(BaseSettings):
12
12
  db_url: str = "sqlite+aiosqlite:///./webgate.db"
13
13
  allowed_origins: str = "*"
14
14
  root_path: str = "" # URL prefix when served behind a reverse proxy (e.g. "/webgate")
15
+ demo_mode: bool = False # Read-only public demo: blocks writes, hides admin UI
15
16
  log_level: str = "info"
16
17
  session_timeout: int = 3600
17
18
  max_upload_size: int = 104857600 # 100MB
@@ -0,0 +1,55 @@
1
+ """Demo mode seeding: pre-populates the demo user and showcase servers.
2
+
3
+ Activated when WEBGATE_DEMO_MODE=true. Idempotent — safe to call on every startup.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+
13
+ from webgate.auth.service import create_user, get_user_by_username
14
+ from webgate.servers.models import ServerCreate
15
+ from webgate.servers.service import create_server, list_servers
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ DEMO_USERNAME = "demo"
21
+ DEMO_PASSWORD = "demo"
22
+
23
+ # Servers pre-registered for the demo. Hostnames must be reachable from the
24
+ # webgate container. In Fly.io they resolve via *.internal DNS.
25
+ DEMO_SERVERS: list[ServerCreate] = [
26
+ ServerCreate(
27
+ name="demo-server",
28
+ hostname="127.0.0.1",
29
+ port=22,
30
+ username="demo",
31
+ password="demo",
32
+ group="demo",
33
+ tags=["demo", "debian"],
34
+ description="Sandbox SSH/SFTP server (debian). Try `ls`, `htop`, browse /home/demo.",
35
+ sftp_read_only=True,
36
+ ),
37
+ ]
38
+
39
+
40
+ async def seed_demo(session: AsyncSession) -> None:
41
+ user = await get_user_by_username(session, DEMO_USERNAME)
42
+ if user is None:
43
+ user = await create_user(session, DEMO_USERNAME, DEMO_PASSWORD, is_admin=False)
44
+ user.must_change_password = False
45
+ user.allowed_groups = json.dumps(["demo"])
46
+ await session.commit()
47
+ logger.info("Created demo user (%s/%s)", DEMO_USERNAME, DEMO_PASSWORD)
48
+
49
+ existing = await list_servers(session, user.id, is_admin=True)
50
+ existing_names = {s.name for s in existing}
51
+ for srv in DEMO_SERVERS:
52
+ if srv.name in existing_names:
53
+ continue
54
+ await create_server(session, srv, user.id)
55
+ logger.info("Seeded demo server: %s -> %s:%s", srv.name, srv.hostname, srv.port)
@@ -118,6 +118,9 @@
118
118
  .cm-editor .cm-scroller { overflow: auto; }
119
119
 
120
120
  /* Login overlay */
121
+ .fz-demo-banner { position: fixed; top: 0; left: 0; right: 0; z-index: 200; background: linear-gradient(90deg, #f59e0b, #ef4444); color: #fff; text-align: center; font-size: 12px; padding: 6px 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
122
+ .fz-demo-banner code { background: rgba(255,255,255,0.25); padding: 1px 5px; border-radius: 3px; font-family: monospace; }
123
+ .fz-demo-banner a { color: #fff; text-decoration: underline; }
121
124
  .fz-login-overlay { position: fixed; inset: 0; background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; z-index: 100; }
122
125
  .fz-login-box { background: var(--modal-bg); border: 1px solid var(--border); border-radius: 12px; padding: 32px; width: 380px; box-shadow: 0 16px 48px rgba(0,0,0,0.4); }
123
126
  .fz-login-box h1 { text-align: center; font-size: 24px; margin-bottom: 4px; color: var(--text-primary); font-weight: 700; }
@@ -191,6 +194,12 @@ window._showToastImpl = function(message, type) {
191
194
 
192
195
  <div x-data="app()" x-cloak @keydown.window="handleKeyboard($event)">
193
196
 
197
+ <!-- Demo banner (shown when WEBGATE_DEMO_MODE=true) -->
198
+ <div x-show="demoMode" class="fz-demo-banner">
199
+ 🎮 <strong>Demo mode</strong> &mdash; login with <code>demo</code> / <code>demo</code> &middot; read-only (servers cannot be added or modified) &middot; state resets hourly &middot;
200
+ <a href="https://github.com/kalexnolasco/webgate" target="_blank" rel="noopener">GitHub</a>
201
+ </div>
202
+
194
203
  <!-- ======================== LOGIN ======================== -->
195
204
  <div x-show="!token" class="fz-login-overlay">
196
205
  <div class="fz-login-box">
@@ -807,6 +816,7 @@ const BASE = location.pathname.replace(/\/(index\.html)?$/, '');
807
816
  function app() {
808
817
  return {
809
818
  theme: localStorage.getItem('webgate_theme') || 'dark',
819
+ demoMode: false,
810
820
  uploadProgress: { active: false, percent: 0, fileName: '', total: 0, loaded: 0 },
811
821
  token: localStorage.getItem('webgate_token') || '',
812
822
  currentUser: null, username: '', password: '',
@@ -879,6 +889,7 @@ function app() {
879
889
 
880
890
  async init() {
881
891
  document.documentElement.dataset.theme = this.theme;
892
+ try { const cfg = await this.apiCall('/api/config'); this.demoMode = !!cfg.demo_mode; } catch {}
882
893
  if (this.token) {
883
894
  await this.fetchMe();
884
895
  if (this.currentUser?.must_change_password) return;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes