webgate 0.2.1__tar.gz → 0.3.0__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.
- {webgate-0.2.1 → webgate-0.3.0}/CHANGELOG.md +35 -0
- webgate-0.3.0/Dockerfile.demo +63 -0
- {webgate-0.2.1 → webgate-0.3.0}/PKG-INFO +25 -1
- {webgate-0.2.1 → webgate-0.3.0}/README.md +22 -0
- {webgate-0.2.1 → webgate-0.3.0}/ROADMAP.md +20 -6
- webgate-0.3.0/fly.toml +48 -0
- {webgate-0.2.1 → webgate-0.3.0}/pyproject.toml +4 -1
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/app.py +33 -1
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/config.py +1 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/db/engine.py +16 -6
- webgate-0.3.0/src/webgate/demo.py +90 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/files/pool.py +13 -1
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/files/routes.py +3 -1
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/servers/models.py +6 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/servers/routes.py +1 -1
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/servers/service.py +50 -2
- webgate-0.3.0/src/webgate/snippets/models.py +34 -0
- webgate-0.3.0/src/webgate/snippets/routes.py +45 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/static/index.html +51 -5
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/terminal/routes.py +8 -1
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/terminal/ssh_session.py +10 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/terminal/ws_handler.py +2 -0
- webgate-0.3.0/tests/__init__.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/.dockerignore +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/.github/workflows/docs.yml +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/.gitignore +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/Dockerfile +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/Dockerfile.ssh-demo +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/LICENSE +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/VERSION +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/compose.dev.yml +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/compose.yml +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/api/auth.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/api/files.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/api/servers.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/api/terminal.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/changelog.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/getting-started/installation.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/getting-started/quickstart.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/guide/files.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/guide/servers.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/guide/split.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/guide/terminal.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/guide/users.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/index.md +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/access-control.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/audit.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/edit-access-control.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/editor.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/light-theme.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/login.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/new-server-form.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/sftp-restricted.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/sftp.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/site-manager.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/split-view.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/ssh-disabled.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/terminal.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/docs/screenshots/users.png +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/mkdocs.yml +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/__init__.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/__main__.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/audit/__init__.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/audit/models.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/audit/service.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/auth/__init__.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/auth/models.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/auth/routes.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/auth/service.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/db/__init__.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/files/__init__.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/files/models.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/files/sftp_service.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/servers/__init__.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/servers/crypto.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/src/webgate/servers/monitor.py +0 -0
- {webgate-0.2.1/src/webgate/terminal → webgate-0.3.0/src/webgate/snippets}/__init__.py +0 -0
- {webgate-0.2.1/tests → webgate-0.3.0/src/webgate/terminal}/__init__.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/tests/conftest.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/tests/test_auth.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/tests/test_files.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/tests/test_servers.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/tests/test_terminal.py +0 -0
- {webgate-0.2.1 → webgate-0.3.0}/uv.lock +0 -0
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.0 (2026-04-15)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **SSH jump host / bastion** -- per-server `jump_via_id` field. Webgate opens a tunneled SSH connection through the bastion using `asyncssh`'s `tunnel=` parameter. Works for both the terminal WebSocket and the SFTP browser. Solves the common "internal servers reachable only through one public bastion" scenario.
|
|
8
|
+
- **SSH command snippets** -- per-user library of named commands. Click a snippet button in the terminal toolbar to send the command (with Enter) to the active session. Right-click to delete, `+` to create.
|
|
9
|
+
- **PostgreSQL support** -- install with `pip install 'webgate[postgres]'` and set `WEBGATE_DB_URL=postgresql+asyncpg://...`. SQLite remains the default. Lightweight migrations are now dialect-aware.
|
|
10
|
+
|
|
11
|
+
### Details
|
|
12
|
+
|
|
13
|
+
- New `Server.jump_via_id` (FK to `servers.id`, nullable) + `resolve_jump_creds()` helper
|
|
14
|
+
- New `Snippet` model + REST CRUD at `/api/snippets`
|
|
15
|
+
- Frontend: dropdown to pick a jump host in the Add/Edit Server modal; `↺` badge in the server card when a jump is configured
|
|
16
|
+
- Frontend: snippet toolbar in the terminal tab (hidden when there are no snippets)
|
|
17
|
+
- Demo seed pre-populates `bastion` + `internal-app` (jump-host pair) and 4 example snippets
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## v0.2.2 (2026-04-15)
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
- **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.
|
|
26
|
+
- **`Dockerfile.demo`** -- single-container image bundling webgate + a sandboxed `sshd` target via supervisord. Ready for free hosting tiers.
|
|
27
|
+
- **`fly.toml`** -- Fly.io configuration for one-command demo deployments (`flyctl deploy`).
|
|
28
|
+
|
|
29
|
+
### Details
|
|
30
|
+
|
|
31
|
+
- New public endpoint `GET /api/config` exposes `{"demo_mode": bool}` for the frontend to render the banner before login
|
|
32
|
+
- `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`)
|
|
33
|
+
- Demo seed (`webgate.demo`) is idempotent and only runs when the flag is on
|
|
34
|
+
- Hourly state reset for the public demo can be done with a cron pinging the container restart, so DB returns to seed state
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
3
38
|
## v0.2.1 (2026-04-15)
|
|
4
39
|
|
|
5
40
|
### 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.
|
|
3
|
+
Version: 0.3.0
|
|
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/
|
|
@@ -42,6 +42,8 @@ Requires-Dist: slowapi>=0.1.9
|
|
|
42
42
|
Requires-Dist: sqlalchemy>=2.0.36
|
|
43
43
|
Requires-Dist: uvicorn[standard]>=0.34.0
|
|
44
44
|
Requires-Dist: websockets>=14.0
|
|
45
|
+
Provides-Extra: postgres
|
|
46
|
+
Requires-Dist: asyncpg>=0.30.0; extra == 'postgres'
|
|
45
47
|
Description-Content-Type: text/markdown
|
|
46
48
|
|
|
47
49
|
# webgate
|
|
@@ -58,6 +60,8 @@ Description-Content-Type: text/markdown
|
|
|
58
60
|
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
61
|
|
|
60
62
|
> **Documentation: [kalexnolasco.github.io/webgate](https://kalexnolasco.github.io/webgate/)**
|
|
63
|
+
>
|
|
64
|
+
> **🎮 Live demo: [webgate-demo.fly.dev](https://webgate-demo.fly.dev/)** — login `demo` / `demo` (read-only, resets hourly)
|
|
61
65
|
|
|
62
66
|
---
|
|
63
67
|
|
|
@@ -449,6 +453,26 @@ All settings are configurable via environment variables with the `WEBGATE_` pref
|
|
|
449
453
|
| `WEBGATE_DB_URL` | `sqlite+aiosqlite:///./webgate.db` | Database URL |
|
|
450
454
|
| `WEBGATE_ALLOWED_ORIGINS` | `*` | CORS origins (comma-separated) |
|
|
451
455
|
| `WEBGATE_ROOT_PATH` | `` (empty) | URL prefix when served behind a reverse proxy at a sub-path (e.g. `/webgate`) |
|
|
456
|
+
| `WEBGATE_DEMO_MODE` | `false` | Read-only public demo: blocks all writes, seeds a `demo`/`demo` user and shows a banner |
|
|
457
|
+
|
|
458
|
+
### PostgreSQL
|
|
459
|
+
|
|
460
|
+
SQLite is the default. To use PostgreSQL instead:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
pip install 'webgate[postgres]' # adds asyncpg
|
|
464
|
+
export WEBGATE_DB_URL='postgresql+asyncpg://user:pass@host:5432/webgate'
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Lightweight migrations are dialect-aware. Schema is created automatically on first start.
|
|
468
|
+
|
|
469
|
+
### Jump host / bastion
|
|
470
|
+
|
|
471
|
+
When adding a server, pick another server from the **Jump Via** dropdown to tunnel the SSH and SFTP connections through it. Useful when the target server is only reachable from a specific bastion. Stored as `jump_via_id` (FK to another `Server` row); `asyncssh`'s native `tunnel=` parameter does the actual chaining.
|
|
472
|
+
|
|
473
|
+
### SSH command snippets
|
|
474
|
+
|
|
475
|
+
Each user has a personal library of named commands. They appear as buttons in the terminal toolbar -- click to send `command + Enter`. Right-click to delete, `+` to create. Stored in the `snippets` table.
|
|
452
476
|
| `WEBGATE_LOG_LEVEL` | `info` | Log level |
|
|
453
477
|
| `WEBGATE_SESSION_TIMEOUT` | `3600` | SSH session timeout (seconds) |
|
|
454
478
|
| `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,26 @@ 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 |
|
|
409
|
+
|
|
410
|
+
### PostgreSQL
|
|
411
|
+
|
|
412
|
+
SQLite is the default. To use PostgreSQL instead:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
pip install 'webgate[postgres]' # adds asyncpg
|
|
416
|
+
export WEBGATE_DB_URL='postgresql+asyncpg://user:pass@host:5432/webgate'
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Lightweight migrations are dialect-aware. Schema is created automatically on first start.
|
|
420
|
+
|
|
421
|
+
### Jump host / bastion
|
|
422
|
+
|
|
423
|
+
When adding a server, pick another server from the **Jump Via** dropdown to tunnel the SSH and SFTP connections through it. Useful when the target server is only reachable from a specific bastion. Stored as `jump_via_id` (FK to another `Server` row); `asyncssh`'s native `tunnel=` parameter does the actual chaining.
|
|
424
|
+
|
|
425
|
+
### SSH command snippets
|
|
426
|
+
|
|
427
|
+
Each user has a personal library of named commands. They appear as buttons in the terminal toolbar -- click to send `command + Enter`. Right-click to delete, `+` to create. Stored in the `snippets` table.
|
|
406
428
|
| `WEBGATE_LOG_LEVEL` | `info` | Log level |
|
|
407
429
|
| `WEBGATE_SESSION_TIMEOUT` | `3600` | SSH session timeout (seconds) |
|
|
408
430
|
| `WEBGATE_MAX_UPLOAD_SIZE` | `104857600` | Max upload size (100 MB) |
|
|
@@ -45,25 +45,39 @@ This document tracks the development plan for **webgate**. Items are organized b
|
|
|
45
45
|
- [x] Drag & drop upload progress bar (XHR with progress events, percentage display)
|
|
46
46
|
- [x] Folder download as ZIP (server-side zip compression, right-click context menu)
|
|
47
47
|
|
|
48
|
-
## v0.
|
|
48
|
+
## v0.2.1 -- Reverse Proxy Sub-Path (Released 2026-04-15)
|
|
49
|
+
|
|
50
|
+
- [x] Serve webgate behind a reverse proxy at any URL prefix (`WEBGATE_ROOT_PATH`)
|
|
51
|
+
- [x] nginx, Apache and Traefik examples in README
|
|
52
|
+
|
|
53
|
+
## v0.2.2 -- Demo Mode (Released 2026-04-15)
|
|
54
|
+
|
|
55
|
+
- [x] `WEBGATE_DEMO_MODE` middleware blocks writes for public read-only deployments
|
|
56
|
+
- [x] `Dockerfile.demo` (webgate + sshd via supervisord) and `fly.toml` for one-command Fly.io deploys
|
|
57
|
+
- [x] Live demo at https://webgate-demo.fly.dev/
|
|
58
|
+
|
|
59
|
+
## v0.3.0 -- Operations Pack (Released 2026-04-15)
|
|
60
|
+
|
|
61
|
+
- [x] **SSH jump host / bastion** (per-server `jump_via_id`, asyncssh tunneling)
|
|
62
|
+
- [x] **SSH command snippets** (per-user library, terminal toolbar, click to send)
|
|
63
|
+
- [x] **PostgreSQL support** (`pip install 'webgate[postgres]'`, dialect-aware migrations)
|
|
64
|
+
- [x] Two-factor authentication (TOTP) -- already in v0.2.x
|
|
65
|
+
- [x] API key authentication -- already in v0.2.x
|
|
66
|
+
|
|
67
|
+
## v0.3.x -- Planned
|
|
49
68
|
|
|
50
69
|
| Feature | Priority | Description |
|
|
51
70
|
|---------|----------|-------------|
|
|
52
71
|
| Shared terminal sessions | High | Multiple users can watch/interact with the same SSH session in real time |
|
|
53
72
|
| Session recording & playback | High | Record terminal sessions for audit trail and training; replay in browser |
|
|
54
|
-
| SSH command snippets | Medium | Save and execute common commands per server or globally; share across team |
|
|
55
73
|
| Webhook notifications | Medium | Fire webhooks on events: user login, SSH connect, file upload, server added |
|
|
56
|
-
| Two-factor authentication (TOTP) | High | Optional TOTP (Google Authenticator, Authy) for user accounts |
|
|
57
74
|
| LDAP / Active Directory | Medium | Authenticate users against corporate LDAP/AD; auto-map groups |
|
|
58
|
-
| API key authentication | Low | Generate API keys for programmatic access (automation, scripts, CI/CD) |
|
|
59
75
|
|
|
60
76
|
## v0.4.0 -- Enterprise & Scale
|
|
61
77
|
|
|
62
78
|
| Feature | Priority | Description |
|
|
63
79
|
|---------|----------|-------------|
|
|
64
|
-
| PostgreSQL support | High | Test, document, and CI-verify PostgreSQL as alternative to SQLite |
|
|
65
80
|
| Multi-instance deployment | Medium | Multiple webgate instances sharing a single database (stateless workers behind LB) |
|
|
66
|
-
| SSH jump host / bastion | High | Connect through a bastion/jump host to reach servers not directly reachable |
|
|
67
81
|
| Custom branding | Low | Configurable logo, application name, and color scheme |
|
|
68
82
|
| Backup/restore UI | Medium | Export/import full application state (users, servers, audit log) from admin panel |
|
|
69
83
|
| Internationalization (i18n) | Low | Multi-language support for the UI (starting with English and Spanish) |
|
webgate-0.3.0/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.
|
|
3
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -42,6 +42,9 @@ dependencies = [
|
|
|
42
42
|
"qrcode[pil]>=8.2",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
|
+
[project.optional-dependencies]
|
|
46
|
+
postgres = ["asyncpg>=0.30.0"]
|
|
47
|
+
|
|
45
48
|
[project.urls]
|
|
46
49
|
Homepage = "https://github.com/kalexnolasco/webgate"
|
|
47
50
|
Documentation = "https://kalexnolasco.github.io/webgate/"
|
|
@@ -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,10 +13,12 @@ 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
|
|
18
20
|
from webgate.servers.routes import router as servers_router
|
|
21
|
+
from webgate.snippets.routes import router as snippets_router
|
|
19
22
|
from webgate.terminal.routes import router as terminal_router
|
|
20
23
|
|
|
21
24
|
|
|
@@ -24,6 +27,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
|
|
24
27
|
await init_db()
|
|
25
28
|
async with async_session_factory() as session:
|
|
26
29
|
await seed_admin(session)
|
|
30
|
+
if settings.demo_mode:
|
|
31
|
+
await seed_demo(session)
|
|
27
32
|
await sftp_pool.start()
|
|
28
33
|
await server_monitor.start()
|
|
29
34
|
yield
|
|
@@ -55,10 +60,37 @@ def create_app() -> FastAPI:
|
|
|
55
60
|
async def health() -> dict[str, str]: # pyright: ignore[reportUnusedFunction]
|
|
56
61
|
return {"status": "ok"}
|
|
57
62
|
|
|
63
|
+
@app.get("/api/config")
|
|
64
|
+
async def public_config() -> dict[str, object]: # pyright: ignore[reportUnusedFunction]
|
|
65
|
+
# Public flags consumed by the frontend before login.
|
|
66
|
+
return {"demo_mode": settings.demo_mode}
|
|
67
|
+
|
|
68
|
+
if settings.demo_mode:
|
|
69
|
+
# In demo mode block any state-changing request on /api/* except login
|
|
70
|
+
# and the WS quick-connect endpoint (no arbitrary SSH targets allowed).
|
|
71
|
+
WRITE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
|
|
72
|
+
WRITE_ALLOWLIST = {"/api/auth/login", "/api/auth/totp/verify"}
|
|
73
|
+
|
|
74
|
+
@app.middleware("http")
|
|
75
|
+
async def _demo_readonly(request: Request, call_next): # pyright: ignore[reportUnusedFunction]
|
|
76
|
+
path = request.url.path
|
|
77
|
+
if request.method in WRITE_METHODS and path.startswith("/api/") and path not in WRITE_ALLOWLIST:
|
|
78
|
+
return JSONResponse(
|
|
79
|
+
status_code=403,
|
|
80
|
+
content={"detail": "Demo mode: write operations are disabled"},
|
|
81
|
+
)
|
|
82
|
+
if path.endswith("/api/ws/terminal/quick"):
|
|
83
|
+
return JSONResponse(
|
|
84
|
+
status_code=403,
|
|
85
|
+
content={"detail": "Demo mode: quick-connect is disabled"},
|
|
86
|
+
)
|
|
87
|
+
return await call_next(request)
|
|
88
|
+
|
|
58
89
|
app.include_router(auth_router)
|
|
59
90
|
app.include_router(servers_router)
|
|
60
91
|
app.include_router(terminal_router)
|
|
61
92
|
app.include_router(files_router)
|
|
93
|
+
app.include_router(snippets_router)
|
|
62
94
|
|
|
63
95
|
app.mount("/", StaticFiles(directory=str(settings.static_dir), html=True), name="static")
|
|
64
96
|
|
|
@@ -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
|
|
@@ -30,21 +30,31 @@ async def get_session() -> AsyncGenerator[AsyncSession]:
|
|
|
30
30
|
yield session
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
33
|
+
# (table, column, sqlite_def, postgres_def)
|
|
34
|
+
_MIGRATIONS: list[tuple[str, str, str, str]] = [
|
|
35
|
+
("servers", "sftp_read_only", "BOOLEAN DEFAULT 0", "BOOLEAN DEFAULT FALSE"),
|
|
36
|
+
("users", "totp_secret", "VARCHAR(255) DEFAULT ''", "VARCHAR(255) DEFAULT ''"),
|
|
37
|
+
("users", "totp_enabled", "BOOLEAN DEFAULT 0", "BOOLEAN DEFAULT FALSE"),
|
|
38
|
+
(
|
|
39
|
+
"servers",
|
|
40
|
+
"jump_via_id",
|
|
41
|
+
"INTEGER REFERENCES servers(id)",
|
|
42
|
+
"INTEGER REFERENCES servers(id)",
|
|
43
|
+
),
|
|
37
44
|
]
|
|
38
45
|
|
|
39
46
|
|
|
40
47
|
async def init_db() -> None:
|
|
48
|
+
dialect = engine.dialect.name # "sqlite", "postgresql", ...
|
|
41
49
|
async with engine.begin() as conn:
|
|
42
50
|
await conn.run_sync(Base.metadata.create_all)
|
|
43
51
|
# Lightweight migrations for columns added after initial release
|
|
44
|
-
for
|
|
52
|
+
for table, column, sqlite_def, pg_def in _MIGRATIONS:
|
|
53
|
+
col_def = pg_def if dialect == "postgresql" else sqlite_def
|
|
54
|
+
sql = f"ALTER TABLE {table} ADD COLUMN {column} {col_def}"
|
|
45
55
|
try:
|
|
46
56
|
await conn.execute(text(sql))
|
|
47
|
-
logger.info("Migration applied: %s",
|
|
57
|
+
logger.info("Migration applied: %s.%s", table, column)
|
|
48
58
|
except Exception:
|
|
49
59
|
pass # Column already exists
|
|
50
60
|
|
|
@@ -0,0 +1,90 @@
|
|
|
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 sqlalchemy import select
|
|
15
|
+
|
|
16
|
+
from webgate.servers.models import ServerCreate
|
|
17
|
+
from webgate.servers.service import create_server, list_servers
|
|
18
|
+
from webgate.snippets.models import Snippet
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEMO_USERNAME = "demo"
|
|
24
|
+
DEMO_PASSWORD = "demo"
|
|
25
|
+
|
|
26
|
+
def _bastion() -> ServerCreate:
|
|
27
|
+
return ServerCreate(
|
|
28
|
+
name="bastion",
|
|
29
|
+
hostname="127.0.0.1",
|
|
30
|
+
port=22,
|
|
31
|
+
username="demo",
|
|
32
|
+
password="demo",
|
|
33
|
+
group="demo",
|
|
34
|
+
tags=["demo", "bastion"],
|
|
35
|
+
description="Public SSH bastion. Other servers proxy through this one.",
|
|
36
|
+
sftp_read_only=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _internal(jump_via_id: int) -> ServerCreate:
|
|
41
|
+
return ServerCreate(
|
|
42
|
+
name="internal-app",
|
|
43
|
+
hostname="127.0.0.1",
|
|
44
|
+
port=22,
|
|
45
|
+
username="demo",
|
|
46
|
+
password="demo",
|
|
47
|
+
group="demo",
|
|
48
|
+
tags=["demo", "internal"],
|
|
49
|
+
description="App server reachable only via the bastion (jump-host demo).",
|
|
50
|
+
sftp_read_only=True,
|
|
51
|
+
jump_via_id=jump_via_id,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def seed_demo(session: AsyncSession) -> None:
|
|
56
|
+
user = await get_user_by_username(session, DEMO_USERNAME)
|
|
57
|
+
if user is None:
|
|
58
|
+
user = await create_user(session, DEMO_USERNAME, DEMO_PASSWORD, is_admin=False)
|
|
59
|
+
user.must_change_password = False
|
|
60
|
+
user.allowed_groups = json.dumps(["demo"])
|
|
61
|
+
await session.commit()
|
|
62
|
+
logger.info("Created demo user (%s/%s)", DEMO_USERNAME, DEMO_PASSWORD)
|
|
63
|
+
|
|
64
|
+
existing = await list_servers(session, user.id, is_admin=True)
|
|
65
|
+
existing_names = {s.name for s in existing}
|
|
66
|
+
|
|
67
|
+
bastion_id: int | None = next((s.id for s in existing if s.name == "bastion"), None)
|
|
68
|
+
if "bastion" not in existing_names:
|
|
69
|
+
bastion = await create_server(session, _bastion(), user.id)
|
|
70
|
+
bastion_id = bastion.id
|
|
71
|
+
logger.info("Seeded demo bastion (id=%s)", bastion_id)
|
|
72
|
+
|
|
73
|
+
if "internal-app" not in existing_names and bastion_id is not None:
|
|
74
|
+
await create_server(session, _internal(bastion_id), user.id)
|
|
75
|
+
logger.info("Seeded internal-app via bastion id=%s", bastion_id)
|
|
76
|
+
|
|
77
|
+
# Seed a few example snippets so the toolbar isn't empty
|
|
78
|
+
existing_snippets = (
|
|
79
|
+
await session.execute(select(Snippet).where(Snippet.user_id == user.id))
|
|
80
|
+
).scalars().all()
|
|
81
|
+
if not existing_snippets:
|
|
82
|
+
for name, cmd, desc in [
|
|
83
|
+
("ls -lah", "ls -lah", "List files (long, human sizes, hidden)"),
|
|
84
|
+
("disk usage", "df -h", "Mounted filesystems with sizes"),
|
|
85
|
+
("top procs", "ps aux --sort=-%mem | head", "Top memory-consuming processes"),
|
|
86
|
+
("uptime", "uptime && uname -a", "Uptime + kernel info"),
|
|
87
|
+
]:
|
|
88
|
+
session.add(Snippet(name=name, command=cmd, description=desc, user_id=user.id))
|
|
89
|
+
await session.commit()
|
|
90
|
+
logger.info("Seeded %d demo snippets", 4)
|
|
@@ -25,6 +25,7 @@ class _PoolEntry:
|
|
|
25
25
|
client: SFTPClient
|
|
26
26
|
last_used: float = field(default_factory=time.monotonic)
|
|
27
27
|
in_use: int = 0
|
|
28
|
+
jump_conn: asyncssh.SSHClientConnection | None = None
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class SFTPPool:
|
|
@@ -63,6 +64,7 @@ class SFTPPool:
|
|
|
63
64
|
username: str,
|
|
64
65
|
password: str | None = None,
|
|
65
66
|
private_key: str | None = None,
|
|
67
|
+
jump_kwargs: dict[str, object] | None = None,
|
|
66
68
|
) -> SFTPClient:
|
|
67
69
|
key = self._key(server_id)
|
|
68
70
|
if key not in self._locks:
|
|
@@ -96,10 +98,15 @@ class SFTPPool:
|
|
|
96
98
|
elif password:
|
|
97
99
|
kwargs["password"] = password
|
|
98
100
|
|
|
101
|
+
jump_conn: asyncssh.SSHClientConnection | None = None
|
|
102
|
+
if jump_kwargs:
|
|
103
|
+
jump_conn = await asyncssh.connect(**jump_kwargs) # type: ignore[arg-type]
|
|
104
|
+
kwargs["tunnel"] = jump_conn
|
|
105
|
+
|
|
99
106
|
conn = await asyncssh.connect(**kwargs) # type: ignore[arg-type]
|
|
100
107
|
client = SFTPClient(conn)
|
|
101
108
|
await client.connect()
|
|
102
|
-
entry = _PoolEntry(conn=conn, client=client, in_use=1)
|
|
109
|
+
entry = _PoolEntry(conn=conn, client=client, in_use=1, jump_conn=jump_conn)
|
|
103
110
|
self._pool[key] = entry
|
|
104
111
|
logger.info("SFTP pool: new connection to %s:%s (server_id=%s)", hostname, port, server_id)
|
|
105
112
|
return client
|
|
@@ -120,6 +127,11 @@ class SFTPPool:
|
|
|
120
127
|
entry.conn.close()
|
|
121
128
|
except Exception:
|
|
122
129
|
pass
|
|
130
|
+
if entry.jump_conn is not None:
|
|
131
|
+
try:
|
|
132
|
+
entry.jump_conn.close()
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
123
135
|
|
|
124
136
|
async def _cleanup_loop(self) -> None:
|
|
125
137
|
while True:
|
|
@@ -25,7 +25,7 @@ from webgate.files.models import (
|
|
|
25
25
|
from webgate.files.pool import sftp_pool
|
|
26
26
|
from webgate.files.sftp_service import SFTPClient, validate_path
|
|
27
27
|
from webgate.servers.models import Server
|
|
28
|
-
from webgate.servers.service import get_server, get_server_credentials
|
|
28
|
+
from webgate.servers.service import get_server, get_server_credentials, resolve_jump_creds
|
|
29
29
|
|
|
30
30
|
router = APIRouter(prefix="/api/files", tags=["files"])
|
|
31
31
|
|
|
@@ -88,6 +88,7 @@ async def _sftp(
|
|
|
88
88
|
allowed_paths = _get_allowed_paths(server)
|
|
89
89
|
read_only = server.sftp_read_only
|
|
90
90
|
password, private_key = get_server_credentials(server)
|
|
91
|
+
jump_kwargs = await resolve_jump_creds(session, server)
|
|
91
92
|
try:
|
|
92
93
|
client = await sftp_pool.acquire(
|
|
93
94
|
server_id,
|
|
@@ -96,6 +97,7 @@ async def _sftp(
|
|
|
96
97
|
username=server.username,
|
|
97
98
|
password=password,
|
|
98
99
|
private_key=private_key,
|
|
100
|
+
jump_kwargs=jump_kwargs,
|
|
99
101
|
)
|
|
100
102
|
yield client, allowed_paths, read_only
|
|
101
103
|
finally:
|
|
@@ -25,6 +25,9 @@ class Server(Base):
|
|
|
25
25
|
sftp_enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
26
26
|
sftp_allowed_paths: Mapped[str] = mapped_column(Text, default="[]") # JSON array of paths
|
|
27
27
|
sftp_read_only: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
28
|
+
jump_via_id: Mapped[int | None] = mapped_column(
|
|
29
|
+
Integer, ForeignKey("servers.id"), nullable=True
|
|
30
|
+
)
|
|
28
31
|
last_connected_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
29
32
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
30
33
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))
|
|
@@ -45,6 +48,7 @@ class ServerCreate(BaseModel):
|
|
|
45
48
|
sftp_enabled: bool = True
|
|
46
49
|
sftp_allowed_paths: list[str] = []
|
|
47
50
|
sftp_read_only: bool = False
|
|
51
|
+
jump_via_id: int | None = None
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
class ServerUpdate(BaseModel):
|
|
@@ -62,6 +66,7 @@ class ServerUpdate(BaseModel):
|
|
|
62
66
|
sftp_enabled: bool | None = None
|
|
63
67
|
sftp_allowed_paths: list[str] | None = None
|
|
64
68
|
sftp_read_only: bool | None = None
|
|
69
|
+
jump_via_id: int | None = None
|
|
65
70
|
|
|
66
71
|
|
|
67
72
|
class ServerOut(BaseModel):
|
|
@@ -78,6 +83,7 @@ class ServerOut(BaseModel):
|
|
|
78
83
|
sftp_enabled: bool
|
|
79
84
|
sftp_allowed_paths: list[str]
|
|
80
85
|
sftp_read_only: bool
|
|
86
|
+
jump_via_id: int | None = None
|
|
81
87
|
last_connected_at: datetime | None
|
|
82
88
|
created_at: datetime
|
|
83
89
|
|
|
@@ -154,7 +154,7 @@ async def test_connectivity(
|
|
|
154
154
|
server = await get_server(session, server_id, current_user.id, **_user_kwargs(current_user))
|
|
155
155
|
if not server:
|
|
156
156
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Server not found")
|
|
157
|
-
success, message = await test_server_connectivity(server)
|
|
157
|
+
success, message = await test_server_connectivity(server, session)
|
|
158
158
|
if success:
|
|
159
159
|
await update_last_connected(session, server)
|
|
160
160
|
return {"success": success, "message": message}
|