feat: add web UI with login, CRUD, admin, and API key management

- Add login page with JWT authentication
- Add dashboard with stats and quick actions
- Add links management page (full CRUD with search)
- Add collections management page
- Add API key management page with copy-to-clipboard
- Add admin user management page (admin only)
- Fix UUID type mismatches across all endpoints
- Add updated_at column to api_keys and audit_log in schema.sql
- Fix DB_PASSWORD default in docker-compose.yml
- Add PyJWT to requirements.txt
- Fix API docs URL (/docs instead of /api/docs)
- Improve JS error handling (show actual messages)
- Rewrite conftest.py with proper DB lifecycle management
- Add 42 new integration tests (84 total, all passing)
  - test_admin.py: 15 tests for admin endpoints
  - test_auth_extended.py: 9 tests for API key CRUD
  - test_tags.py: 12 tests for tag endpoints
  - test_sync.py: 6 tests for sync endpoints
This commit is contained in:
DavidSaylor
2026-05-21 07:21:49 -05:00
parent 09d30427f4
commit 77b076c7d7
31 changed files with 2740 additions and 213 deletions

View File

@@ -5,8 +5,9 @@ LinkSyncServer - Authentication Endpoints
import hashlib
import os
import secrets
import uuid
from datetime import datetime, timedelta
from typing import Optional
from typing import List, Optional
import bcrypt
import jwt
@@ -178,6 +179,32 @@ async def logout():
return {"message": "Logged out successfully"}
@router.get("/api-keys", response_model=List[dict])
async def list_api_keys(
current_user: dict = Depends(get_current_user),
):
db = get_session()
try:
user = db.query(User).filter(User.username == current_user["username"]).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
api_keys = db.query(ApiKey).filter(ApiKey.user_id == user.id).order_by(ApiKey.created_at.desc()).all()
return [
{
"id": str(key.id),
"key_id": str(key.id),
"name": key.name,
"is_active": key.is_active,
"expires_at": key.expires_at.isoformat() if key.expires_at else None,
"created_at": key.created_at.isoformat() if key.created_at else None,
}
for key in api_keys
]
finally:
db.close()
@router.post("/api-key", response_model=ApiKeyResponse)
async def create_api_key(
name: str = "default",
@@ -204,7 +231,7 @@ async def create_api_key(
return {
"api_key": raw_key,
"key_id": api_key.id,
"key_id": str(api_key.id),
"name": api_key.name,
"expires_at": api_key.expires_at.isoformat() if api_key.expires_at else None,
}
@@ -223,14 +250,19 @@ async def get_api_key_info(
if not user:
raise HTTPException(status_code=404, detail="User not found")
try:
parsed_key_id = uuid.UUID(key_id)
except (ValueError, AttributeError):
raise HTTPException(status_code=404, detail="API key not found")
api_key = db.query(ApiKey).filter(
ApiKey.id == key_id, ApiKey.user_id == user.id
ApiKey.id == parsed_key_id, ApiKey.user_id == user.id
).first()
if not api_key:
raise HTTPException(status_code=404, detail="API key not found")
return {
"key_id": api_key.id,
"key_id": str(api_key.id),
"name": api_key.name,
"is_active": api_key.is_active,
"expires_at": api_key.expires_at.isoformat() if api_key.expires_at else None,
@@ -251,8 +283,13 @@ async def delete_api_key(
if not user:
raise HTTPException(status_code=404, detail="User not found")
try:
parsed_key_id = uuid.UUID(key_id)
except (ValueError, AttributeError):
raise HTTPException(status_code=404, detail="API key not found")
api_key = db.query(ApiKey).filter(
ApiKey.id == key_id, ApiKey.user_id == user.id
ApiKey.id == parsed_key_id, ApiKey.user_id == user.id
).first()
if not api_key:
raise HTTPException(status_code=404, detail="API key not found")