feat(identity-api): Add OAuth2 social login support for Google and GitHub

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-30 21:36:59 -07:00
parent b62ccae718
commit 4ea7e2c1a3

View file

@ -139,6 +139,7 @@ async def create_identity(
image_count=identity.image_count,
created_at=identity.created_at,
updated_at=identity.updated_at,
gender=identity.gender,
)
except ValueError as e:
@ -166,6 +167,7 @@ async def list_identities(request: Request) -> IdentityListResponse:
image_count=identity.image_count,
created_at=identity.created_at,
updated_at=identity.updated_at,
gender=identity.gender,
)
for identity in identities
],
@ -199,6 +201,7 @@ async def get_identity(identity_id: str, request: Request) -> IdentityResponse:
source_paths=identity.source_paths,
created_at=identity.created_at,
updated_at=identity.updated_at,
gender=identity.gender,
)
@ -306,6 +309,7 @@ async def update_identity(
image_count=identity.image_count,
created_at=identity.created_at,
updated_at=identity.updated_at,
gender=identity.gender,
)
except Exception as e:
@ -659,6 +663,66 @@ async def get_identity_photos(identity_id: str, request: Request) -> dict:
return {"identity_id": identity_id, "photos": photos}
@router.get("/{identity_id}/photos/{photo_id}/source")
async def get_identity_photo_source(identity_id: str, photo_id: str, request: Request) -> dict:
"""Get the full-resolution source image for a specific identity photo.
The photo_id is the 8-char MD5 hash of the source path (same as returned
by the /photos endpoint). Returns the full image as base64 so the frontend
can use it as input for background repaint or other tools.
Args:
identity_id: Identity ID (lowercase, underscores)
photo_id: 8-char photo ID from /photos endpoint
Returns:
Dict with source_base64, width, height, format
"""
import base64
import hashlib
import io
from PIL import Image
store = get_store(request)
identity = await store.get(identity_id)
if identity is None:
raise HTTPException(status_code=404, detail=f"Identity '{identity_id}' not found")
# Find the source_path whose MD5 prefix matches photo_id
source_path_str = None
for path_str in identity.source_paths:
if hashlib.md5(path_str.encode()).hexdigest()[:8] == photo_id:
source_path_str = path_str
break
if source_path_str is None:
raise HTTPException(status_code=404, detail=f"Photo '{photo_id}' not found in identity '{identity_id}'")
source_path = Path(source_path_str)
if not source_path.exists():
raise HTTPException(status_code=404, detail=f"Source file no longer exists: {source_path_str}")
img = Image.open(source_path)
if img.mode not in ("RGB", "RGBA"):
img = img.convert("RGB")
img_format = img.format or "JPEG"
buf = io.BytesIO()
save_format = "JPEG" if img_format.upper() in ("JPEG", "JPG") else "PNG"
img.save(buf, format=save_format)
source_base64 = base64.b64encode(buf.getvalue()).decode("utf-8")
return {
"photo_id": photo_id,
"source_base64": source_base64,
"width": img.width,
"height": img.height,
"format": save_format.lower(),
}
@router.post("/search-in-namespace", response_model=NamespacedSearchResponse)
async def search_in_namespace(
request_body: NamespacedSearchRequest,