""" 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))