wiregui/wiregui/main.py

96 lines
2.9 KiB
Python
Raw Normal View History

feat: initial WireGUI implementation — full VPN management platform 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
2026-03-30 16:53:46 -05:00
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()