Files
gitrusprusandCursor 5811ecb134 Add VPN admin tab with subscription proxy via cp-vpn for selected sources.
Persist settings in CA, expose /admin/vpn and /internal/vpn, and route Telegram (and optional web/nlp) traffic through mihomo SOCKS when enabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 23:41:33 +03:00

138 lines
4.5 KiB
Python

"""VPN settings persistence helpers."""
from __future__ import annotations
from contracts.queues import known_source_types
from contracts.vpn import VpnSettings
from sqlalchemy.orm import Session
from ..models import VpnSettingsRow
from ..schemas import VpnSettingsAdminRead, VpnSettingsInternalRead, VpnSettingsUpdate
SINGLETON_ID = 1
def _hint_secret(value: str | None) -> str | None:
if not value:
return None
if len(value) <= 4:
return "****"
return f"…{value[-4:]}"
def ensure_vpn_settings(db: Session) -> VpnSettingsRow:
row = db.query(VpnSettingsRow).filter(VpnSettingsRow.id == SINGLETON_ID).first()
if row is not None:
return row
row = VpnSettingsRow(
id=SINGLETON_ID,
enabled=False,
mode="subscription",
subscription_interval_seconds=3600,
proxied_source_types=[],
)
db.add(row)
db.commit()
db.refresh(row)
return row
def row_to_contract(row: VpnSettingsRow) -> VpnSettings:
return VpnSettings.model_validate(
{
"enabled": row.enabled,
"mode": row.mode,
"subscription_url": row.subscription_url,
"subscription_interval_seconds": row.subscription_interval_seconds,
"host": row.host,
"port": row.port,
"username": row.username,
"password": row.password,
"proxied_source_types": row.proxied_source_types or [],
}
)
def to_admin_read(row: VpnSettingsRow) -> VpnSettingsAdminRead:
return VpnSettingsAdminRead(
enabled=row.enabled,
mode=row.mode, # type: ignore[arg-type]
subscription_url_set=bool(row.subscription_url),
subscription_url_hint=_hint_secret(row.subscription_url),
subscription_interval_seconds=row.subscription_interval_seconds,
host=row.host,
port=row.port,
username=row.username,
password_set=bool(row.password),
proxied_source_types=list(row.proxied_source_types or []),
available_source_types=known_source_types(),
)
def to_internal_read(row: VpnSettingsRow) -> VpnSettingsInternalRead:
return VpnSettingsInternalRead(
enabled=row.enabled,
mode=row.mode, # type: ignore[arg-type]
subscription_url=row.subscription_url,
subscription_interval_seconds=row.subscription_interval_seconds,
host=row.host,
port=row.port,
username=row.username,
password=row.password,
proxied_source_types=list(row.proxied_source_types or []),
)
def apply_vpn_update(db: Session, payload: VpnSettingsUpdate) -> VpnSettingsRow:
row = ensure_vpn_settings(db)
data = {
"enabled": row.enabled if payload.enabled is None else payload.enabled,
"mode": row.mode if payload.mode is None else payload.mode,
"subscription_url": row.subscription_url,
"subscription_interval_seconds": (
row.subscription_interval_seconds
if payload.subscription_interval_seconds is None
else payload.subscription_interval_seconds
),
"host": row.host if payload.host is None else (payload.host.strip() or None),
"port": row.port if payload.port is None else payload.port,
"username": (
row.username
if payload.username is None
else (payload.username.strip() or None)
),
"password": row.password,
"proxied_source_types": (
list(row.proxied_source_types or [])
if payload.proxied_source_types is None
else payload.proxied_source_types
),
}
if payload.clear_subscription_url:
data["subscription_url"] = None
elif payload.subscription_url is not None:
stripped = payload.subscription_url.strip()
data["subscription_url"] = stripped or None
if payload.clear_password:
data["password"] = None
elif payload.password is not None:
stripped = payload.password.strip()
data["password"] = stripped or None
validated = VpnSettings.model_validate(data)
row.enabled = validated.enabled
row.mode = validated.mode
row.subscription_url = validated.subscription_url
row.subscription_interval_seconds = validated.subscription_interval_seconds
row.host = validated.host
row.port = validated.port
row.username = validated.username
row.password = validated.password
row.proxied_source_types = validated.proxied_source_types
db.commit()
db.refresh(row)
return row