Files
Nexus/server/background/ip_allowlist_refresh.py
T
Your Name 488838c989 feat: separate subscription/manual IPs + master enable/disable toggle
Data model (separate storage):
- login_allowlist_enabled: 'true'/'false' master switch
- login_subscription_url: URL to fetch every 2h
- login_subscription_ips: last fetch result (replaced entirely each refresh)
- login_manual_ips: manually added (never overwritten by refresh)
- login_allowed_ips: combined (used by auth for legacy compat)

ip_allowlist_refresh.py:
- Saves to login_subscription_ips (replace) + rebuilds login_allowed_ips

auth_service.py:
- Login check: only runs when LOGIN_ALLOWLIST_ENABLED is 'true'
- Combines subscription_ips + manual_ips at auth time (no stale combined key needed)

settings.py:
- POST /ip-allowlist/toggle: quick enable/disable endpoint
- GET /ip-allowlist: returns subscription_ips, manual_ips separately
- POST /ip-allowlist: saves manual_ips + optional subscription_url + optional enabled

settings.html:
- Toggle switch: enable/disable with warning if list is empty
- Green notice when enabled (warns about self-lockout risk)
- Grey notice when disabled
- Subscription IPs section: read-only, auto-refresh badge
- Manual IPs section: deletable per-IP, clear all button

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 18:19:47 +08:00

100 lines
3.5 KiB
Python

"""Background task: refresh login subscription IPs every 2 hours.
Data model:
login_subscription_url — URL to fetch (configurable in settings)
login_subscription_ips — IPs from last fetch (completely replaced each time)
login_manual_ips — manually added (never touched by auto-refresh)
login_allowed_ips — combined: subscription + manual (used by auth check)
login_allowlist_enabled — "true" / "false" master switch
"""
import asyncio
import logging
logger = logging.getLogger("nexus.ip_allowlist_refresh")
REFRESH_INTERVAL = 7200 # 2 hours
_last_refresh_at: str = ""
_last_subscription_count: int = 0
async def ip_allowlist_refresh_loop():
"""Every 2 hours, re-fetch subscription IPs and rebuild combined allowlist."""
logger.info("IP allowlist refresh loop started (interval=%sh)", REFRESH_INTERVAL // 3600)
# Initial refresh shortly after startup
await asyncio.sleep(5)
await _do_refresh()
while True:
await asyncio.sleep(REFRESH_INTERVAL)
await _do_refresh()
async def _do_refresh():
"""Fetch subscription, replace subscription IPs, rebuild combined list."""
global _last_refresh_at, _last_subscription_count
from server.config import settings
from server.infrastructure.subscription_parser import parse_subscription_content
sub_url = (settings.LOGIN_SUBSCRIPTION_URL or "").strip()
if not sub_url:
return
logger.info("Refreshing subscription IPs from: %s", sub_url[:80])
try:
import httpx
async with httpx.AsyncClient(timeout=20.0) as client:
resp = await client.get(sub_url, follow_redirects=True)
if resp.status_code != 200:
logger.warning("Subscription returned HTTP %s", resp.status_code)
return
new_sub_ips = parse_subscription_content(resp.text)
except Exception as e:
logger.error("Subscription fetch failed: %s", e)
return
# Manual IPs (always preserved)
manual_raw = (settings.LOGIN_MANUAL_IPS or "").strip()
manual_ips = [ip.strip() for ip in manual_raw.split(",") if ip.strip()]
# Combined list: subscription first, then manual (dedup)
seen: set[str] = set()
combined: list[str] = []
for ip in new_sub_ips + manual_ips:
if ip not in seen:
seen.add(ip)
combined.append(ip)
sub_val = ",".join(new_sub_ips)
combined_val = ",".join(combined)
try:
from server.infrastructure.database.session import AsyncSessionLocal
from server.infrastructure.database.setting_repo import SettingRepositoryImpl
async with AsyncSessionLocal() as session:
repo = SettingRepositoryImpl(session)
# Subscription IPs — replaced entirely
await repo.set("login_subscription_ips", sub_val)
# Combined — for auth check
await repo.set("login_allowed_ips", combined_val)
settings.LOGIN_SUBSCRIPTION_IPS = sub_val
settings.LOGIN_ALLOWED_IPS = combined_val
from datetime import datetime, timezone
_last_refresh_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
_last_subscription_count = len(new_sub_ips)
logger.info(
"Subscription IPs refreshed: %d → combined %d (+ %d manual)",
len(new_sub_ips), len(combined), len(manual_ips),
)
except Exception as e:
logger.error("Failed to save subscription IPs: %s", e)
def get_last_refresh_time() -> str:
return _last_refresh_at
def get_subscription_count() -> int:
return _last_subscription_count