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:
@@ -55,6 +55,13 @@ def get_current_user_id(request: Request) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_uuid(id_str: str):
|
||||
try:
|
||||
return uuid.UUID(id_str) if isinstance(id_str, str) else id_str
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def log_audit(db, action: str, entity_type: str, entity_id: str, user_id: Optional[str], old_value=None, new_value=None):
|
||||
try:
|
||||
audit = AuditLog(
|
||||
@@ -105,7 +112,10 @@ async def list_bookmarks(
|
||||
async def get_bookmark(bookmark_id: str):
|
||||
db = get_session()
|
||||
try:
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
|
||||
parsed_id = parse_uuid(bookmark_id)
|
||||
if parsed_id is None:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == parsed_id).first()
|
||||
if not bookmark:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
return bookmark.to_dict()
|
||||
@@ -117,9 +127,14 @@ async def get_bookmark(bookmark_id: str):
|
||||
async def create_bookmark(data: BookmarkCreate, request: Request):
|
||||
db = get_session()
|
||||
try:
|
||||
user_id = get_current_user_id(request)
|
||||
user_id = None
|
||||
username = get_current_user_id(request)
|
||||
if username:
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if user:
|
||||
user_id = user.id
|
||||
bookmark = Bookmark(
|
||||
id=str(uuid.uuid4()),
|
||||
id=uuid.uuid4(),
|
||||
url=data.url,
|
||||
title=data.title,
|
||||
description=data.description,
|
||||
@@ -144,7 +159,10 @@ async def create_bookmark(data: BookmarkCreate, request: Request):
|
||||
async def update_bookmark(bookmark_id: str, data: BookmarkUpdate, request: Request):
|
||||
db = get_session()
|
||||
try:
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
|
||||
parsed_id = parse_uuid(bookmark_id)
|
||||
if parsed_id is None:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == parsed_id).first()
|
||||
if not bookmark:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
|
||||
@@ -155,8 +173,9 @@ async def update_bookmark(bookmark_id: str, data: BookmarkUpdate, request: Reque
|
||||
|
||||
db.commit()
|
||||
db.refresh(bookmark)
|
||||
user_id = get_current_user_id(request)
|
||||
log_audit(db, "update", "Bookmark", bookmark_id, user_id, old_value=old_value, new_value=bookmark.to_dict())
|
||||
username = get_current_user_id(request)
|
||||
user = db.query(User).filter(User.username == username).first() if username else None
|
||||
log_audit(db, "update", "Bookmark", bookmark.id, user.id if user else None, old_value=old_value, new_value=bookmark.to_dict())
|
||||
return bookmark.to_dict()
|
||||
finally:
|
||||
db.close()
|
||||
@@ -166,16 +185,20 @@ async def update_bookmark(bookmark_id: str, data: BookmarkUpdate, request: Reque
|
||||
async def delete_bookmark(bookmark_id: str, request: Request):
|
||||
db = get_session()
|
||||
try:
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
|
||||
parsed_id = parse_uuid(bookmark_id)
|
||||
if parsed_id is None:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == parsed_id).first()
|
||||
if not bookmark:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
|
||||
old_value = bookmark.to_dict()
|
||||
db.delete(bookmark)
|
||||
db.commit()
|
||||
user_id = get_current_user_id(request)
|
||||
log_audit(db, "delete", "Bookmark", bookmark_id, user_id, old_value=old_value)
|
||||
return {"message": "Bookmark deleted successfully", "deleted_id": bookmark_id}
|
||||
username = get_current_user_id(request)
|
||||
user = db.query(User).filter(User.username == username).first() if username else None
|
||||
log_audit(db, "delete", "Bookmark", bookmark.id, user.id if user else None, old_value=old_value)
|
||||
return {"message": "Bookmark deleted successfully", "deleted_id": str(bookmark.id)}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -188,7 +211,10 @@ class TagList(BaseModel):
|
||||
async def add_tags(bookmark_id: str, data: TagList, request: Request):
|
||||
db = get_session()
|
||||
try:
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
|
||||
parsed_id = parse_uuid(bookmark_id)
|
||||
if parsed_id is None:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == parsed_id).first()
|
||||
if not bookmark:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
|
||||
@@ -211,7 +237,10 @@ async def add_tags(bookmark_id: str, data: TagList, request: Request):
|
||||
async def remove_tags(bookmark_id: str, data: TagList, request: Request):
|
||||
db = get_session()
|
||||
try:
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
|
||||
parsed_id = parse_uuid(bookmark_id)
|
||||
if parsed_id is None:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
bookmark = db.query(Bookmark).filter(Bookmark.id == parsed_id).first()
|
||||
if not bookmark:
|
||||
raise HTTPException(status_code=404, detail="Bookmark not found")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user