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
42 lines
1.3 KiB
Python
42 lines
1.3 KiB
Python
"""API token authentication — Bearer token via Authorization header."""
|
|
|
|
import hashlib
|
|
import secrets
|
|
|
|
from loguru import logger
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlmodel import select
|
|
|
|
from wiregui.models.api_token import ApiToken
|
|
from wiregui.models.user import User
|
|
from wiregui.utils.time import utcnow
|
|
|
|
|
|
def generate_api_token() -> tuple[str, str]:
|
|
"""Generate a new API token. Returns (plaintext_token, token_hash)."""
|
|
plaintext = secrets.token_urlsafe(32)
|
|
token_hash = hashlib.sha256(plaintext.encode()).hexdigest()
|
|
return plaintext, token_hash
|
|
|
|
|
|
async def resolve_bearer_token(session: AsyncSession, token: str) -> User | None:
|
|
"""Look up a Bearer token and return the associated user, or None."""
|
|
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
|
result = await session.execute(
|
|
select(ApiToken).where(ApiToken.token_hash == token_hash)
|
|
)
|
|
api_token = result.scalar_one_or_none()
|
|
if api_token is None:
|
|
return None
|
|
|
|
# Check expiry
|
|
if api_token.expires_at and api_token.expires_at < utcnow():
|
|
logger.debug("API token expired for user_id={}", api_token.user_id)
|
|
return None
|
|
|
|
# Resolve user
|
|
user = await session.get(User, api_token.user_id)
|
|
if user is None or user.disabled_at is not None:
|
|
return None
|
|
|
|
return user
|