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:
Oliver Hofmann 2026-04-27 21:48:26 +02:00
parent bf694b79e2
commit cfa874a4c3
9 changed files with 188 additions and 214 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Python
__pycache__/
*.pyc
*.pyo
.venv/
*.egg-info/
# Environment & secrets
.env
*.env.local
# Databases
*.db
*.sqlite3
# IDE
.idea/
.vscode/
# Frontend build
frontend/node_modules/
frontend/dist/
# Misc
config.json

152
README.md
View File

@ -4,28 +4,41 @@ Ein Reverse-Proxy für Ollama mit API-Key-Authentifizierung und Quota-Management
## Features ## Features
- 🔑 API-Key-Authentifizierung (Bearer Token oder `sk-` Prefix) - API-Key-Authentifizierung (Bearer Token oder `sk-`-Prefix)
- 📊 Quota-Management (Tokens & Requests pro Tag/Monat) - Quota-Management mit getrennten Tages- und Monatslimits (Tokens & Requests)
- 📈 Usage-Tracking in Echtzeit - Token-Zählung via tiktoken (cl100k_base)
- 🖥️ Web-Admin-Oberfläche für User & Quotas - Usage-Tracking mit automatischem täglichem/monatlichem Reset
- 🔒 Benutzer-Management - Web-Admin-Oberfläche für User- und Quota-Verwaltung
- OpenAI-kompatibler `/v1/chat/completions`-Endpunkt
## Sicherheit
- Passwörter mit bcrypt gehasht
- API-Keys als SHA-256-Hash in der DB Plaintext wird nur einmalig bei Erstellung zurückgegeben
- Admin-Zugriff über `is_admin`-Flag in der DB, nicht über Hardcoded-Namen
- CORS-Origins konfigurierbar via `ALLOWED_ORIGINS`
- Quota-Check atomar mit `SELECT FOR UPDATE` (kein TOCTOU-Race)
## Installation & Start ## Installation & Start
### Voraussetzungen ### Voraussetzungen
- Python 3.12+ - Python 3.12+
- PostgreSQL 16+ - PostgreSQL 16+ (oder SQLite für Entwicklung)
- Node.js 18+ (für Frontend) - Node.js 18+ (für Frontend)
### lokal mit SQLite (Entwicklung) ### Lokal mit SQLite (Entwicklung)
```bash ```bash
# Backend # Backend
cd backend cd backend
pip install -r requirements.txt
python init_db.py python init_db.py
python setup_admin.py python setup_admin.py
uvicorn main:app --reload uvicorn main:app --reload --port 8000
# Admin-API (in neuem Terminal)
uvicorn admin:app --reload --port 8001
# Frontend (in neuem Terminal) # Frontend (in neuem Terminal)
cd frontend cd frontend
@ -33,7 +46,7 @@ npm install
npm run dev npm run dev
``` ```
### mit PostgreSQL & Docker (Produktion) ### Mit PostgreSQL & Docker (Produktion)
```bash ```bash
docker compose up -d docker compose up -d
@ -41,18 +54,25 @@ docker compose exec backend python init_db.py
docker compose exec backend python setup_admin.py docker compose exec backend python setup_admin.py
``` ```
## Usage ## Konfiguration
### Mit API-Key authentifizieren `.env`-Datei im `backend/`-Verzeichnis anlegen:
```bash ```env
curl -X POST http://localhost:8000/api/generate \ DATABASE_URL=postgresql://user:pass@host:5432/db
-H "Authorization: sk-xxxxxx" \ OLLAMA_URL=http://ollama:11434
-H "Content-Type: application/json" \ ALLOWED_ORIGINS=https://admin.example.com
-d '{"model":"llama3","prompt":"Say hello"}'
``` ```
Oder mit Bearer Token: | Variable | Standard | Beschreibung |
|----------|----------|--------------|
| `DATABASE_URL` | PostgreSQL lokal | DB-Verbindungsstring; `sqlite:///` für SQLite |
| `OLLAMA_URL` | `http://localhost:11434` | Adresse der Ollama-Instanz |
| `ALLOWED_ORIGINS` | `http://localhost:5173` | Kommagetrennte CORS-Origins für die Admin-UI |
## Proxy-Endpunkte
Alle Endpunkte erfordern einen gültigen API-Key im `Authorization`-Header.
```bash ```bash
curl -X POST http://localhost:8000/api/generate \ curl -X POST http://localhost:8000/api/generate \
@ -61,50 +81,48 @@ curl -X POST http://localhost:8000/api/generate \
-d '{"model":"llama3","prompt":"Say hello"}' -d '{"model":"llama3","prompt":"Say hello"}'
``` ```
### Admin API Endpoints | Endpunkt | Beschreibung |
|----------|--------------|
| `POST /api/generate` | Ollama generate |
| `POST /api/chat` | Ollama chat |
| `GET /api/tags` | Verfügbare Modelle |
| `GET /v1/models` | Modelle (OpenAI-Format) |
| `POST /v1/chat/completions` | Chat (OpenAI-Format) |
| Endpoint | Method | Description | ## Admin-API (Port 8001)
|----------|--------|-------------|
| `/api/users` | GET | Liste aller User | Alle Endpunkte erfordern einen API-Key eines Nutzers mit `is_admin=true`.
| `/api/users` | POST | Neuen User erstellen |
| `/api/api-keys` | GET | Liste aller API-Keys | | Endpunkt | Methode | Beschreibung |
| `/api/api-keys` | POST | Neuen API-Key erstellen | |----------|---------|--------------|
| `/api/users` | GET | Alle User auflisten |
| `/api/users` | POST | Neuen User anlegen |
| `/api/api-keys` | GET | Alle API-Keys auflisten |
| `/api/api-keys` | POST | Neuen API-Key erstellen (Plaintext einmalig in Response) |
| `/api/api-keys/{id}/deactivate` | PUT | API-Key deaktivieren | | `/api/api-keys/{id}/deactivate` | PUT | API-Key deaktivieren |
| `/api/quotas/{user_id}` | PUT | Quota für User setzen | | `/api/quotas/{user_id}` | PUT | Quota für User setzen |
### Quota-Beispiele ### Quota setzen
```json ```bash
// Quota setzen (per PUT /api/quotas/{user_id}) curl -X PUT http://localhost:8001/api/quotas/1 \
{ -H "Authorization: Bearer sk-admin-key" \
"daily_tokens": 1000000, -H "Content-Type: application/json" \
"monthly_tokens": 10000000, -d '{
"daily_requests": 1000, "daily_tokens": 1000000,
"monthly_requests": 10000 "monthly_tokens": 10000000,
} "daily_requests": 1000,
"monthly_requests": 10000
}'
``` ```
## Konfiguration `null` für ein Limit bedeutet unbegrenzt.
### Umgebungsvariablen ## Tests
Im `backend/` Verzeichnis `.env` Datei erstellen: ```bash
cd backend
```env python -m pytest tests/ -v
OLLAMA_URL=http://ollama:11434
DATABASE_URL=postgresql://user:pass@host:5432/db
SECRET_KEY=your-secret-key-min-32-chars
```
### Docker Compose
`docker-compose.yml` anpassen:
```yaml
environment:
- DATABASE_URL=postgresql://ollama:password@db:5432/ollama_proxy
- OLLAMA_URL=http://ollama:11434
- SECRET_KEY=your-secret-key
``` ```
## Projektstruktur ## Projektstruktur
@ -112,23 +130,25 @@ environment:
``` ```
llm_quota/ llm_quota/
├── backend/ ├── backend/
│ ├── main.py # Proxy-Server (Ollama API forwarden) │ ├── main.py # Proxy-Server
│ ├── admin.py # Admin API (User/Quota Management) │ ├── admin.py # Admin-API
│ ├── database.py # DB-Verbindung & Session │ ├── database.py # DB-Verbindung & Session
│ ├── models.py # SQLAlchemy Models │ ├── models.py # SQLAlchemy-Modelle
│ ├── schemas.py # Pydantic Schemas │ ├── schemas.py # Pydantic-Schemas
│ ├── crud.py # Database Operations │ ├── crud.py # DB-Operationen & Token-Zählung
│ ├── types.py # Response Types │ ├── init_db.py # Tabellen anlegen
│ ├── setup_admin.py # Admin-User & API-Key erstellen
│ ├── requirements.txt │ ├── requirements.txt
│ ├── Dockerfile │ ├── Dockerfile
│ └── setup_admin.py # Admin User erstellen │ └── tests/
│ ├── conftest.py # Fixtures
│ ├── test_auth.py # Authentifizierungs-Tests
│ └── test_quota.py # Quota- & Token-Tests
├── frontend/ ├── frontend/
│ ├── src/ │ └── src/
│ │ ├── main.jsx │ ├── main.jsx
│ │ └── styles.css │ └── styles.css
│ ├── index.html ├── .gitignore
│ ├── package.json
│ └── vite.config.js
└── docker-compose.yml └── docker-compose.yml
``` ```

View File

@ -2,7 +2,7 @@ import secrets
import hashlib import hashlib
import bcrypt import bcrypt
import tiktoken import tiktoken
from datetime import datetime from datetime import datetime, timezone
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from models import APIKey, User, Quota, Usage 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.add(usage)
db.flush() db.flush()
now = datetime.utcnow() now = datetime.now(timezone.utc)
if usage.daily_reset_at.date() < now.date(): if usage.daily_reset_at.date() < now.date():
usage.tokens_used_today = 0 usage.tokens_used_today = 0

View File

@ -8,7 +8,7 @@ try:
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else: else:
engine = create_engine(DATABASE_URL) engine = create_engine(DATABASE_URL)
except: except Exception:
DATABASE_URL = "sqlite:///./test.db" DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})

View File

@ -1,7 +1,9 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, BigInteger from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, BigInteger
from datetime import datetime from datetime import datetime, timezone
from database import Base from database import Base
_now = lambda: datetime.now(timezone.utc)
class User(Base): class User(Base):
__tablename__ = "users" __tablename__ = "users"
@ -11,7 +13,7 @@ class User(Base):
hashed_password = Column(String) hashed_password = Column(String)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False) is_admin = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), default=_now)
class APIKey(Base): class APIKey(Base):
__tablename__ = "api_keys" __tablename__ = "api_keys"
@ -21,7 +23,7 @@ class APIKey(Base):
key = Column(String, unique=True, index=True) key = Column(String, unique=True, index=True)
user_id = Column(Integer, ForeignKey("users.id")) user_id = Column(Integer, ForeignKey("users.id"))
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), default=_now)
class Quota(Base): class Quota(Base):
__tablename__ = "quotas" __tablename__ = "quotas"
@ -32,7 +34,7 @@ class Quota(Base):
monthly_tokens = Column(BigInteger, nullable=True) monthly_tokens = Column(BigInteger, nullable=True)
daily_requests = Column(Integer, nullable=True) daily_requests = Column(Integer, nullable=True)
monthly_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): class Usage(Base):
__tablename__ = "usage" __tablename__ = "usage"
@ -43,5 +45,5 @@ class Usage(Base):
tokens_used_month = Column(BigInteger, default=0) tokens_used_month = Column(BigInteger, default=0)
requests_today = Column(Integer, default=0) requests_today = Column(Integer, default=0)
requests_month = Column(Integer, default=0) requests_month = Column(Integer, default=0)
daily_reset_at = Column(DateTime, default=datetime.utcnow) daily_reset_at = Column(DateTime(timezone=True), default=_now)
monthly_reset_at = Column(DateTime, default=datetime.utcnow) monthly_reset_at = Column(DateTime(timezone=True), default=_now)

View File

@ -1,49 +1,41 @@
import pytest import pytest
from fastapi.testclient import TestClient
import tempfile
import os import os
from pathlib import Path
def create_test_db(): os.environ.setdefault("OLLAMA_URL", "http://127.0.0.1:9999")
"""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
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(): def _setup_db():
"""Setup test database with required data."""
from database import Base, engine, SessionLocal 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 from crud import create_api_key, hash_password
# Create tables Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
db = SessionLocal() db = SessionLocal()
# Create test user
test_user = User( test_user = User(
username="testuser", username="testuser",
email="test@example.com", email="test@example.com",
hashed_password=hash_password("test123"), hashed_password=hash_password("test123"),
is_active=True is_active=True,
) )
db.add(test_user) db.add(test_user)
db.commit() db.commit()
db.refresh(test_user) 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") _, raw_key = create_api_key(db, test_user.id, "test-key")
os.environ["TEST_API_KEY"] = raw_key os.environ["TEST_API_KEY"] = raw_key
# Create admin user
admin_user = User( admin_user = User(
username="admin", username="admin",
email="admin@example.com", email="admin@example.com",
@ -55,33 +47,34 @@ def setup_test_db():
db.commit() db.commit()
db.refresh(admin_user) 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") _, admin_raw_key = create_api_key(db, admin_user.id, "admin-key")
os.environ["ADMIN_API_KEY"] = admin_raw_key os.environ["ADMIN_API_KEY"] = admin_raw_key
db.close() db.close()
return raw_key, admin_raw_key
def teardown_test_db(): def _teardown_db():
"""Clean up test database and environment."""
from database import engine from database import engine
from models import Base from models import Base
Base.metadata.drop_all(bind=engine) Base.metadata.drop_all(bind=engine)
os.environ.pop("TEST_API_KEY", None) os.environ.pop("TEST_API_KEY", None)
os.environ.pop("ADMIN_API_KEY", None) os.environ.pop("ADMIN_API_KEY", None)
@pytest.fixture(scope="session")
@pytest.fixture(scope="function")
def test_client(): def test_client():
"""Create test client with test database.""" _setup_db()
db_path = create_test_db()
setup_test_db()
from main import app from main import app
client = TestClient(app) from fastapi.testclient import TestClient
client = TestClient(app, raise_server_exceptions=False)
yield client yield client
_teardown_db()
teardown_test_db()
cleanup_test_db(db_path)

View File

@ -1,97 +1,21 @@
import pytest
import os import os
from unittest.mock import AsyncMock, patch 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): def test_auth_middleware_missing_auth(test_client):
response = test_client.post("/api/generate", json={"model": "llama3", "prompt": "test"}) response = test_client.post("/api/generate", json={"model": "llama3", "prompt": "test"})
assert response.status_code == 401 assert response.status_code == 401
def test_auth_middleware_invalid_key(test_client): def test_auth_middleware_invalid_key(test_client):
response = test_client.post( response = test_client.post(
"/api/generate", "/api/generate",
headers={"Authorization": "sk-invalid-key"}, headers={"Authorization": "sk-invalid-key"},
json={"model": "llama3", "prompt": "test"} json={"model": "llama3", "prompt": "test"},
) )
assert response.status_code == 401 assert response.status_code == 401
@patch("main.proxy_request", new_callable=AsyncMock) @patch("main.proxy_request", new_callable=AsyncMock)
def test_auth_middleware_valid_key(mock_proxy, test_client): def test_auth_middleware_valid_key(mock_proxy, test_client):
mock_proxy.return_value.status_code = 200 mock_proxy.return_value.status_code = 200
@ -101,6 +25,6 @@ def test_auth_middleware_valid_key(mock_proxy, test_client):
response = test_client.post( response = test_client.post(
"/api/generate", "/api/generate",
headers={"Authorization": os.environ.get("TEST_API_KEY", "")}, headers={"Authorization": os.environ.get("TEST_API_KEY", "")},
json={"model": "llama3", "prompt": "test"} json={"model": "llama3", "prompt": "test"},
) )
assert response.status_code == 200 assert response.status_code == 200

View File

@ -1,6 +1,6 @@
import pytest import pytest
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
os.environ.setdefault("OLLAMA_URL", "http://127.0.0.1:9999") 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 # Backdate daily_reset_at to yesterday
usage = db.query(Usage).filter(Usage.user_id == user_id).first() 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() db.commit()
# Should pass again after reset # 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) check_and_increment_quota(db, user_id, tokens=50, requests=1)
usage = db.query(Usage).filter(Usage.user_id == user_id).first() 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() db.commit()
check_and_increment_quota(db, user_id, tokens=50, requests=1) 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) check_and_increment_quota(db, user_id, tokens=90, requests=1)
usage = db.query(Usage).filter(Usage.user_id == user_id).first() 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() db.commit()
assert check_and_increment_quota(db, user_id, tokens=90, requests=1) is True 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 # 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 = 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 usage.tokens_used_today = 80
db.commit() db.commit()

View File

@ -2,28 +2,38 @@ import React, { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import './styles.css'; import './styles.css';
const maskKey = (key) => `••••••••${key.slice(-4)}`;
function App() { function App() {
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [apiKeys, setApiKeys] = useState([]); const [apiKeys, setApiKeys] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => { useEffect(() => {
fetchUsers(); Promise.all([fetchUsers(), fetchApiKeys()]).finally(() => setLoading(false));
fetchApiKeys();
setLoading(false);
}, []); }, []);
const fetchUsers = async () => { const fetchUsers = async () => {
const res = await axios.get('/api/users'); try {
setUsers(res.data); const res = await axios.get('/api/users');
setUsers(res.data);
} catch (err) {
setError('Benutzer konnten nicht geladen werden.');
}
}; };
const fetchApiKeys = async () => { const fetchApiKeys = async () => {
const res = await axios.get('/api/api-keys'); try {
setApiKeys(res.data); const res = await axios.get('/api/api-keys');
setApiKeys(res.data);
} catch (err) {
setError('API-Keys konnten nicht geladen werden.');
}
}; };
if (loading) return <div>Loading...</div>; if (loading) return <div>Loading...</div>;
if (error) return <div className="error">{error}</div>;
return ( return (
<div className="container"> <div className="container">
@ -70,7 +80,7 @@ function App() {
<tr key={key.id}> <tr key={key.id}>
<td>{key.id}</td> <td>{key.id}</td>
<td>{key.name}</td> <td>{key.name}</td>
<td>{key.key}</td> <td>{maskKey(key.key)}</td>
<td>{key.user_id}</td> <td>{key.user_id}</td>
<td>{key.is_active ? 'Active' : 'Inactive'}</td> <td>{key.is_active ? 'Active' : 'Inactive'}</td>
</tr> </tr>