152 lines
4.3 KiB
Python
152 lines
4.3 KiB
Python
"""
|
|
LinkSyncServer - Sync Endpoint for Browser Extension
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException, status
|
|
from typing import List, Dict
|
|
import jwt
|
|
import logging
|
|
from datetime import datetime
|
|
import json
|
|
|
|
from models.base import Bookmark, Collection, get_engine
|
|
from api.parsers.bookmarks import BookmarkParser
|
|
from api.parsers.sync import SyncParser
|
|
import os
|
|
|
|
router = APIRouter(prefix="/api/v1/sync", tags=["Sync"])
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Get database and secrets
|
|
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///links.db")
|
|
SECRET_KEY = os.environ.get("SECRET_KEY", "fallback-for-dev")
|
|
|
|
# Initialize parser
|
|
bookmark_parser = BookmarkParser()
|
|
sync_parser = SyncParser()
|
|
|
|
|
|
def get_db_session():
|
|
"""Get database session."""
|
|
from sqlalchemy.pool import StaticPool
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import create_engine
|
|
|
|
engine = create_engine(
|
|
DATABASE_URL,
|
|
connect_args={'check_same_thread': False}
|
|
)
|
|
return Session(engine)
|
|
|
|
|
|
def validate_request_token(request_token: str) -> Dict:
|
|
"""
|
|
Validate sync request token.
|
|
|
|
Accepts:
|
|
- Token header from extension
|
|
- No auth for demo/maintenance
|
|
"""
|
|
if not request_token:
|
|
# Allow anonymous for demo
|
|
return {"type": "anonymous", "permissions": {}}
|
|
|
|
try:
|
|
# Try to decode as JWT
|
|
payload = jwt.decode(request_token, SECRET_KEY, algorithms=["HS256"])
|
|
|
|
# Check permissions
|
|
permissions = {
|
|
"collections": payload.get("permissions", {}).get("collections", []),
|
|
"bookmarks": payload.get("permissions", {}).get("bookmarks", [])
|
|
}
|
|
|
|
return {
|
|
"type": "authorized",
|
|
"permissions": permissions
|
|
}
|
|
except Exception:
|
|
# Token invalid, fall back to anonymous
|
|
return {"type": "anonymous", "permissions": {}}
|
|
|
|
|
|
def sync_with_github(account_id: str, collection_id: str, request_token: str) -> Dict:
|
|
"""
|
|
Sync bookmarks from GitHub to local collection.
|
|
|
|
Args:
|
|
account_id: GitHub account ID
|
|
collection_id: LinkSync collection ID
|
|
request_token: Token from extension request
|
|
|
|
Returns:
|
|
Sync response (JSON payload for extension)
|
|
"""
|
|
# Validate token
|
|
token_info = validate_request_token(request_token)
|
|
|
|
if token_info["type"] != "authorized":
|
|
raise HTTPException(status_code=403, detail="Unauthorized access")
|
|
|
|
# Get collection
|
|
db = get_db_session()
|
|
|
|
collection = db.query(Collection).filter(Collection.id == collection_id).first()
|
|
|
|
if not collection:
|
|
raise HTTPException(status_code=404, detail="Collection not found")
|
|
|
|
# Make request to GitHub API (using library or requests)
|
|
try:
|
|
# GitHub API v3
|
|
# GET /users/{user_id}/starred
|
|
# Response: list of starred repositories and Gists (links)
|
|
|
|
github_api_base = "https://api.github.com"
|
|
starred_response = requests.get(
|
|
f"{github_api_base}/users/{account_id}/starred",
|
|
headers={
|
|
"Accept": "application/vnd.github.v3+json"
|
|
}
|
|
)
|
|
|
|
if starred_response.status_code != 200:
|
|
raise HTTPException(status_code=502, detail="Failed to fetch GitHub data")
|
|
|
|
github_links = starred_response.json()
|
|
|
|
# Parse GitHub data
|
|
github_bookmarks = sync_parser.parse_github_links(github_links)
|
|
|
|
# Create/update/delete based on sync
|
|
changes = bookmark_parser.parse_sync(
|
|
github_bookmarks, collection_id
|
|
)
|
|
|
|
# Commit changes
|
|
db.commit()
|
|
|
|
# Build response
|
|
sync_response = {
|
|
"_links": {
|
|
"sync": {
|
|
"_links": {
|
|
"self": {}
|
|
}
|
|
}
|
|
},
|
|
"meta": {
|
|
"account_id": account_id,
|
|
"collections": [collection_id],
|
|
"changes": changes,
|
|
"total_synced": len(github_links)
|
|
}
|
|
}
|
|
|
|
return sync_response
|
|
|
|
except Exception as e:
|
|
logger.error(f"Sync error: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|