Fix medium/low priority review items; update README
Medium: - Frontend: Error-Handling in fetchUsers/fetchApiKeys (try/catch) - Frontend: Loading-Race behoben (Promise.all + .finally) - Frontend: API-Keys maskiert (nur letzte 4 Zeichen sichtbar) - Tests: Setup-Code aus test_auth.py in conftest.py konsolidiert - Tests: Fixture-Scope vereinheitlicht (function statt session) Low: - bare except in database.py → except Exception - datetime.utcnow → datetime.now(timezone.utc) durchgängig - DateTime(timezone=True) in allen Modell-Spalten - .gitignore hinzugefügt (.env, *.db, __pycache__, .idea, node_modules) Docs: - README aktualisiert (Sicherheit, Konfiguration, Projektstruktur, Tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
bf694b79e2
commit
cfa874a4c3
+2
-2
@@ -2,7 +2,7 @@ import secrets
|
||||
import hashlib
|
||||
import bcrypt
|
||||
import tiktoken
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import Session
|
||||
from models import APIKey, User, Quota, Usage
|
||||
|
||||
@@ -89,7 +89,7 @@ def check_and_increment_quota(db: Session, user_id: int, tokens: int = 0, reques
|
||||
db.add(usage)
|
||||
db.flush()
|
||||
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
if usage.daily_reset_at.date() < now.date():
|
||||
usage.tokens_used_today = 0
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ try:
|
||||
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
else:
|
||||
engine = create_engine(DATABASE_URL)
|
||||
except:
|
||||
except Exception:
|
||||
DATABASE_URL = "sqlite:///./test.db"
|
||||
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
|
||||
|
||||
+8
-6
@@ -1,7 +1,9 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, BigInteger
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from database import Base
|
||||
|
||||
_now = lambda: datetime.now(timezone.utc)
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
@@ -11,7 +13,7 @@ class User(Base):
|
||||
hashed_password = Column(String)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
created_at = Column(DateTime(timezone=True), default=_now)
|
||||
|
||||
class APIKey(Base):
|
||||
__tablename__ = "api_keys"
|
||||
@@ -21,7 +23,7 @@ class APIKey(Base):
|
||||
key = Column(String, unique=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
created_at = Column(DateTime(timezone=True), default=_now)
|
||||
|
||||
class Quota(Base):
|
||||
__tablename__ = "quotas"
|
||||
@@ -32,7 +34,7 @@ class Quota(Base):
|
||||
monthly_tokens = Column(BigInteger, nullable=True)
|
||||
daily_requests = Column(Integer, nullable=True)
|
||||
monthly_requests = Column(Integer, nullable=True)
|
||||
reset_at = Column(DateTime, default=datetime.utcnow)
|
||||
reset_at = Column(DateTime(timezone=True), default=_now)
|
||||
|
||||
class Usage(Base):
|
||||
__tablename__ = "usage"
|
||||
@@ -43,5 +45,5 @@ class Usage(Base):
|
||||
tokens_used_month = Column(BigInteger, default=0)
|
||||
requests_today = Column(Integer, default=0)
|
||||
requests_month = Column(Integer, default=0)
|
||||
daily_reset_at = Column(DateTime, default=datetime.utcnow)
|
||||
monthly_reset_at = Column(DateTime, default=datetime.utcnow)
|
||||
daily_reset_at = Column(DateTime(timezone=True), default=_now)
|
||||
monthly_reset_at = Column(DateTime(timezone=True), default=_now)
|
||||
|
||||
+35
-42
@@ -1,49 +1,41 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def create_test_db():
|
||||
"""Create a temporary SQLite database for tests."""
|
||||
temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
|
||||
temp_db.close()
|
||||
os.environ["DATABASE_URL"] = f"sqlite:///{temp_db.name}"
|
||||
return temp_db.name
|
||||
os.environ.setdefault("OLLAMA_URL", "http://127.0.0.1:9999")
|
||||
|
||||
def cleanup_test_db(db_path):
|
||||
"""Remove the temporary database."""
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
os.environ.pop("DATABASE_URL", None)
|
||||
|
||||
def setup_test_db():
|
||||
"""Setup test database with required data."""
|
||||
def _setup_db():
|
||||
from database import Base, engine, SessionLocal
|
||||
from models import User, APIKey, Quota, Usage
|
||||
from models import User, Quota
|
||||
from crud import create_api_key, hash_password
|
||||
|
||||
# Create tables
|
||||
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
db = SessionLocal()
|
||||
|
||||
# Create test user
|
||||
|
||||
test_user = User(
|
||||
username="testuser",
|
||||
email="test@example.com",
|
||||
hashed_password=hash_password("test123"),
|
||||
is_active=True
|
||||
is_active=True,
|
||||
)
|
||||
db.add(test_user)
|
||||
db.commit()
|
||||
db.refresh(test_user)
|
||||
|
||||
# Create API key for test user
|
||||
|
||||
db.add(Quota(
|
||||
user_id=test_user.id,
|
||||
daily_tokens=1_000_000,
|
||||
monthly_tokens=10_000_000,
|
||||
daily_requests=1000,
|
||||
monthly_requests=10000,
|
||||
))
|
||||
db.commit()
|
||||
|
||||
_, raw_key = create_api_key(db, test_user.id, "test-key")
|
||||
os.environ["TEST_API_KEY"] = raw_key
|
||||
|
||||
# Create admin user
|
||||
admin_user = User(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
@@ -55,33 +47,34 @@ def setup_test_db():
|
||||
db.commit()
|
||||
db.refresh(admin_user)
|
||||
|
||||
# Create admin API key
|
||||
db.add(Quota(
|
||||
user_id=admin_user.id,
|
||||
daily_tokens=10_000_000,
|
||||
monthly_tokens=100_000_000,
|
||||
daily_requests=10000,
|
||||
monthly_requests=100000,
|
||||
))
|
||||
db.commit()
|
||||
|
||||
_, admin_raw_key = create_api_key(db, admin_user.id, "admin-key")
|
||||
os.environ["ADMIN_API_KEY"] = admin_raw_key
|
||||
|
||||
|
||||
db.close()
|
||||
|
||||
return raw_key, admin_raw_key
|
||||
|
||||
def teardown_test_db():
|
||||
"""Clean up test database and environment."""
|
||||
def _teardown_db():
|
||||
from database import engine
|
||||
from models import Base
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
|
||||
os.environ.pop("TEST_API_KEY", None)
|
||||
os.environ.pop("ADMIN_API_KEY", None)
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def test_client():
|
||||
"""Create test client with test database."""
|
||||
db_path = create_test_db()
|
||||
setup_test_db()
|
||||
|
||||
_setup_db()
|
||||
from main import app
|
||||
client = TestClient(app)
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
client = TestClient(app, raise_server_exceptions=False)
|
||||
yield client
|
||||
|
||||
teardown_test_db()
|
||||
cleanup_test_db(db_path)
|
||||
_teardown_db()
|
||||
|
||||
@@ -1,106 +1,30 @@
|
||||
import pytest
|
||||
import os
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from fastapi.testclient import TestClient
|
||||
from main import app
|
||||
from database import Base, engine, SessionLocal
|
||||
from models import User, APIKey, Quota
|
||||
from crud import create_api_key, hash_password
|
||||
|
||||
os.environ["OLLAMA_URL"] = "http://127.0.0.1:9999"
|
||||
|
||||
def setup_test_db():
|
||||
"""Setup test database with required data."""
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
db = SessionLocal()
|
||||
|
||||
test_user = User(
|
||||
username="testuser",
|
||||
email="test@example.com",
|
||||
hashed_password=hash_password("test123"),
|
||||
is_active=True
|
||||
)
|
||||
db.add(test_user)
|
||||
db.commit()
|
||||
db.refresh(test_user)
|
||||
|
||||
quota = Quota(
|
||||
user_id=test_user.id,
|
||||
daily_tokens=1000000,
|
||||
monthly_tokens=10000000,
|
||||
daily_requests=1000,
|
||||
monthly_requests=10000
|
||||
)
|
||||
db.add(quota)
|
||||
db.commit()
|
||||
|
||||
api_key_record, raw_key = create_api_key(db, test_user.id, "test-key")
|
||||
os.environ["TEST_API_KEY"] = raw_key
|
||||
|
||||
admin_user = User(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
hashed_password=hash_password("admin123"),
|
||||
is_active=True,
|
||||
is_admin=True,
|
||||
)
|
||||
db.add(admin_user)
|
||||
db.commit()
|
||||
db.refresh(admin_user)
|
||||
|
||||
admin_quota = Quota(
|
||||
user_id=admin_user.id,
|
||||
daily_tokens=10000000,
|
||||
monthly_tokens=100000000,
|
||||
daily_requests=10000,
|
||||
monthly_requests=100000
|
||||
)
|
||||
db.add(admin_quota)
|
||||
db.commit()
|
||||
|
||||
_, admin_raw_key = create_api_key(db, admin_user.id, "admin-key")
|
||||
os.environ["ADMIN_API_KEY"] = admin_raw_key
|
||||
|
||||
db.close()
|
||||
|
||||
return os.environ["TEST_API_KEY"], os.environ["ADMIN_API_KEY"]
|
||||
|
||||
def teardown_test_db():
|
||||
"""Clean up test database and environment."""
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
os.environ.pop("TEST_API_KEY", None)
|
||||
os.environ.pop("ADMIN_API_KEY", None)
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def test_client():
|
||||
setup_test_db()
|
||||
client = TestClient(app, raise_server_exceptions=False)
|
||||
yield client
|
||||
teardown_test_db()
|
||||
|
||||
def test_auth_middleware_missing_auth(test_client):
|
||||
response = test_client.post("/api/generate", json={"model": "llama3", "prompt": "test"})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_auth_middleware_invalid_key(test_client):
|
||||
response = test_client.post(
|
||||
"/api/generate",
|
||||
headers={"Authorization": "sk-invalid-key"},
|
||||
json={"model": "llama3", "prompt": "test"}
|
||||
json={"model": "llama3", "prompt": "test"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@patch("main.proxy_request", new_callable=AsyncMock)
|
||||
def test_auth_middleware_valid_key(mock_proxy, test_client):
|
||||
mock_proxy.return_value.status_code = 200
|
||||
mock_proxy.return_value.json = lambda: {"response": "success"}
|
||||
mock_proxy.return_value.headers = {}
|
||||
|
||||
|
||||
response = test_client.post(
|
||||
"/api/generate",
|
||||
headers={"Authorization": os.environ.get("TEST_API_KEY", "")},
|
||||
json={"model": "llama3", "prompt": "test"}
|
||||
json={"model": "llama3", "prompt": "test"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
os.environ.setdefault("OLLAMA_URL", "http://127.0.0.1:9999")
|
||||
|
||||
@@ -126,7 +126,7 @@ def test_daily_reset_restores_access(db):
|
||||
|
||||
# Backdate daily_reset_at to yesterday
|
||||
usage = db.query(Usage).filter(Usage.user_id == user_id).first()
|
||||
usage.daily_reset_at = datetime.utcnow() - timedelta(days=1)
|
||||
usage.daily_reset_at = datetime.now(timezone.utc) - timedelta(days=1)
|
||||
db.commit()
|
||||
|
||||
# Should pass again after reset
|
||||
@@ -140,7 +140,7 @@ def test_daily_reset_does_not_affect_monthly_counter(db):
|
||||
check_and_increment_quota(db, user_id, tokens=50, requests=1)
|
||||
|
||||
usage = db.query(Usage).filter(Usage.user_id == user_id).first()
|
||||
usage.daily_reset_at = datetime.utcnow() - timedelta(days=1)
|
||||
usage.daily_reset_at = datetime.now(timezone.utc) - timedelta(days=1)
|
||||
db.commit()
|
||||
|
||||
check_and_increment_quota(db, user_id, tokens=50, requests=1)
|
||||
@@ -154,7 +154,7 @@ def test_monthly_reset_restores_access(db):
|
||||
check_and_increment_quota(db, user_id, tokens=90, requests=1)
|
||||
|
||||
usage = db.query(Usage).filter(Usage.user_id == user_id).first()
|
||||
usage.monthly_reset_at = datetime.utcnow() - timedelta(days=32)
|
||||
usage.monthly_reset_at = datetime.now(timezone.utc) - timedelta(days=32)
|
||||
db.commit()
|
||||
|
||||
assert check_and_increment_quota(db, user_id, tokens=90, requests=1) is True
|
||||
@@ -168,7 +168,7 @@ def test_failed_quota_check_still_commits_reset(db):
|
||||
|
||||
# Backdate so a reset fires, but the new request still exceeds the limit
|
||||
usage = db.query(Usage).filter(Usage.user_id == user_id).first()
|
||||
usage.daily_reset_at = datetime.utcnow() - timedelta(days=1)
|
||||
usage.daily_reset_at = datetime.now(timezone.utc) - timedelta(days=1)
|
||||
usage.tokens_used_today = 80
|
||||
db.commit()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user