feat: initial WireGUI implementation — full VPN management platform
Some checks failed
CI / test (push) Failing after 26s
CI / release (push) Has been skipped
CI / docker (push) Has been skipped

Complete Python/NiceGUI rewrite of the Wirezone (Elixir/Phoenix) VPN
management platform. All 10 implementation phases delivered.

Core stack:
- NiceGUI reactive UI with SQLModel ORM on PostgreSQL (asyncpg)
- Alembic migrations, Valkey/Redis cache, pydantic-settings config
- WireGuard management via subprocess (wg/ip/nft CLIs)
- 164 tests passing, 35% code coverage

Features:
- User/device/rule CRUD with admin and unprivileged roles
- Full device config form with per-device WG overrides
- WireGuard client config generation with QR codes
- REST API (v0) with Bearer token auth for all resources
- TOTP MFA with QR registration and challenge flow
- OIDC SSO with authlib (provider registry, auto-create users)
- Magic link passwordless sign-in via email
- SAML SP-initiated SSO with IdP metadata parsing
- WebAuthn/FIDO2 security key registration
- nftables firewall with per-user chains and masquerade
- Background tasks: WG stats polling, VPN session expiry,
  OIDC token refresh, WAN connectivity checks
- Startup reconciliation (DB ↔ WireGuard state sync)
- In-memory notification system with header badge
- Admin UI: users, devices, rules, settings (3 tabs), diagnostics
- Loguru logging with optional timestamped file output

Deployment:
- Multi-stage Dockerfile (python:3.13-slim)
- Docker Compose prod stack (bridge networking, NET_ADMIN, nftables)
- Forgejo CI: tests → semantic versioning → Docker registry push
- Health endpoint at /api/health
This commit is contained in:
Stefano Bertelli 2026-03-30 16:53:46 -05:00
commit 0546b44507
109 changed files with 11793 additions and 0 deletions

95
wiregui/main.py Normal file
View file

@ -0,0 +1,95 @@
from loguru import logger
from nicegui import app, ui
from wiregui.api.v0 import router as api_router
from wiregui.auth.seed import ensure_server_keypair, seed_admin
from wiregui.config import get_settings
from wiregui.db import init_db
from wiregui.logging import setup_logging
# Mount REST API
app.include_router(api_router, prefix="/api")
@app.get("/api/health")
async def health():
return {"status": "ok"}
# Import pages so their @ui.page decorators register routes
import wiregui.pages.account # noqa: F401
import wiregui.pages.admin.devices # noqa: F401
import wiregui.pages.admin.diagnostics # noqa: F401
import wiregui.pages.admin.rules # noqa: F401
import wiregui.pages.admin.settings # noqa: F401
import wiregui.pages.admin.users # noqa: F401
import wiregui.pages.auth_magic # noqa: F401
import wiregui.pages.auth_oidc # noqa: F401
import wiregui.pages.auth_saml # noqa: F401
import wiregui.pages.devices # noqa: F401
import wiregui.pages.home # noqa: F401
import wiregui.pages.login # noqa: F401
import wiregui.pages.mfa_challenge # noqa: F401
async def startup() -> None:
settings = get_settings()
setup_logging(log_to_file=settings.log_to_file)
await init_db()
await seed_admin()
await ensure_server_keypair()
# Register OIDC providers from config
from wiregui.auth.oidc import register_providers
await register_providers()
from wiregui.tasks import register_task
from wiregui.tasks.oidc_refresh import oidc_refresh_loop
from wiregui.tasks.connectivity import connectivity_loop
from wiregui.tasks.vpn_session import vpn_session_loop
# Always run these tasks (even without WG for OIDC refresh and connectivity)
register_task(oidc_refresh_loop(), name="oidc-refresh")
register_task(connectivity_loop(), name="connectivity-check")
if settings.wg_enabled:
from wiregui.services.firewall import setup_base_tables, setup_masquerade
from wiregui.services.wireguard import configure_interface, ensure_interface
from wiregui.tasks.reconcile import reconcile
from wiregui.tasks.stats import stats_loop
await ensure_interface()
await configure_interface()
await setup_base_tables()
await setup_masquerade()
await reconcile()
register_task(stats_loop(), name="wg-stats")
register_task(vpn_session_loop(), name="vpn-session-expiry")
else:
logger.info("WireGuard disabled (WG_WG_ENABLED=false) — running in UI-only mode")
logger.info("WireGUI ready")
async def shutdown() -> None:
from wiregui.tasks import cancel_all
await cancel_all()
app.on_startup(startup)
app.on_shutdown(shutdown)
def main() -> None:
settings = get_settings()
ui.run(
host=settings.host,
port=settings.port,
title="WireGUI",
storage_secret=settings.secret_key,
reload=True,
)
if __name__ in {"__main__", "__mp_main__"}:
main()