fix: migrations, auth, providers health check, E2E tests, remove neko-sama
- 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:
+41
-48
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user