fix: migrations, auth, providers health check, E2E tests, remove neko-sama
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled

- Add proper Alembic initial migration (0001_initial_schema.py)
- Migrate refresh tokens from JSON file to SQLite (RefreshTokenTable)
- Remove Neko-Sama provider entirely (redirects to Gupy, not a host)
- Fix provider health check always showing UNKNOWN
  - Run check_all_health() on startup
  - Fix POST /providers/health/check background task bug
  - Add HTMX refresh after manual health check trigger
- Fix anime search relevance scoring with MIN_RELEVANCE_THRESHOLD=0.5
- Replace bare 'except:' with 'except Exception:' across codebase
- Add Playwright E2E test suite (12 tests, auth setup, helpers)
- Fix toast container blocking clicks via pointer-events: none
- Remove obsolete Jest/Vite test files and config
- Clean up obsolete test_watchlist scripts
- Update sonarr model comment for active providers
This commit is contained in:
Kimi Agent
2026-05-12 11:45:56 +00:00
parent 693615a7dc
commit 520be53901
47 changed files with 654 additions and 3437 deletions
+41 -48
View File
@@ -1,9 +1,7 @@
"""User authentication and management system with SQLModel support"""
import os
import hashlib
from datetime import datetime, timedelta
from typing import Optional, Dict, List
from typing import Optional
from jose import jwt
from passlib.context import CryptContext
import logging
@@ -11,7 +9,7 @@ from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
from sqlmodel import Session, select
from app.database import engine
from app.models.auth import UserTable
from app.models.auth import UserTable, RefreshTokenTable
from app.config import get_settings
logger = logging.getLogger(__name__)
@@ -189,33 +187,32 @@ def get_current_user(credentials: HTTPAuthorizationCredentials) -> UserTable:
raise HTTPException(status_code=401, detail="Invalid authentication credentials")
# Refresh tokens storage
REFRESH_TOKENS_FILE = "config/refresh_tokens.json"
def _get_refresh_token(token_id: str) -> Optional[RefreshTokenTable]:
"""Get a refresh token from the database by token_id"""
with Session(engine) as session:
statement = select(RefreshTokenTable).where(RefreshTokenTable.token_id == token_id)
return session.exec(statement).first()
def _load_refresh_tokens() -> Dict[str, dict]:
"""Load refresh tokens from file"""
import json
try:
if os.path.exists(REFRESH_TOKENS_FILE):
with open(REFRESH_TOKENS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
logger.error(f"Error loading refresh tokens: {e}")
return {}
def _save_refresh_token(token: RefreshTokenTable):
"""Save or update a refresh token in the database"""
with Session(engine) as session:
session.add(token)
session.commit()
def _save_refresh_tokens(tokens: Dict[str, dict]):
"""Save refresh tokens to file"""
import json
try:
os.makedirs(os.path.dirname(REFRESH_TOKENS_FILE), exist_ok=True)
with open(REFRESH_TOKENS_FILE, "w", encoding="utf-8") as f:
json.dump(tokens, f, indent=2, ensure_ascii=False, default=str)
except Exception as e:
logger.error(f"Error saving refresh tokens: {e}")
def _revoke_refresh_token_db(token_id: str) -> bool:
"""Revoke a refresh token in the database"""
with Session(engine) as session:
statement = select(RefreshTokenTable).where(RefreshTokenTable.token_id == token_id)
db_token = session.exec(statement).first()
if not db_token:
return False
db_token.revoked = True
db_token.revoked_at = datetime.now()
session.add(db_token)
session.commit()
return True
def _get_jwt_config() -> dict:
@@ -267,15 +264,15 @@ def create_access_refresh_tokens(data: dict) -> tuple[str, str]:
refresh_data, jwt_config["SECRET_KEY"], algorithm=jwt_config["ALGORITHM"]
)
# Store refresh token mapping
refresh_tokens = _load_refresh_tokens()
refresh_tokens[token_id] = {
"username": data["sub"],
"token_id": token_id,
"created_at": datetime.now().isoformat(),
"expires_at": refresh_expire.isoformat(),
}
_save_refresh_tokens(refresh_tokens)
# Store refresh token in database
db_token = RefreshTokenTable(
token_id=token_id,
username=data["sub"],
created_at=datetime.now(),
expires_at=refresh_expire,
revoked=False,
)
_save_refresh_token(db_token)
return access_token, refresh_token
@@ -305,15 +302,18 @@ def verify_refresh_token(token: str) -> Optional[str]:
if not username or not token_id:
return None
# Check if token exists in storage
refresh_tokens = _load_refresh_tokens()
stored_token = refresh_tokens.get(token_id)
# Check if token exists in database
stored_token = _get_refresh_token(token_id)
if not stored_token:
return None
# Verify token hasn't been revoked or expired
if stored_token.get("revoked"):
if stored_token.revoked:
return None
# Also check expiration in database
if stored_token.expires_at and stored_token.expires_at < datetime.now():
return None
return username
@@ -341,14 +341,7 @@ def revoke_refresh_token(token: str) -> bool:
if not token_id:
return False
refresh_tokens = _load_refresh_tokens()
if token_id in refresh_tokens:
refresh_tokens[token_id]["revoked"] = True
refresh_tokens[token_id]["revoked_at"] = datetime.now().isoformat()
_save_refresh_tokens(refresh_tokens)
return True
return False
return _revoke_refresh_token_db(token_id)
except JWTError:
return False