feat: implement Instagram clone SocialPhoto API

- FastAPI backend with SQLite database
- JWT authentication (register, login)
- User profiles with follow/unfollow
- Posts with image upload and likes/dislikes
- Comments with likes
- Global and personalized feed
- Comprehensive pytest test suite (37 tests)

TASK-ID: 758f4029-702
This commit is contained in:
OpenClaw Agent
2026-04-16 03:20:48 +00:00
parent 8cbc4000ac
commit a3eca3b7da
95 changed files with 2767 additions and 1 deletions

205
app/routes/users.py Normal file
View File

@@ -0,0 +1,205 @@
"""User routes for SocialPhoto."""
import sqlite3
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.auth import get_current_user_id
from app.database import get_db, row_to_dict
from app.models import PostResponse, UserResponse, UserStats
router = APIRouter(prefix="/users", tags=["Users"])
security = HTTPBearer()
def _get_user_with_stats(conn: sqlite3.Connection, user_id: int) -> dict:
"""Get user data with stats."""
cursor = conn.cursor()
# Get user
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
row = cursor.fetchone()
if not row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
user = row_to_dict(row)
# Get posts count
cursor.execute("SELECT COUNT(*) as count FROM posts WHERE user_id = ?", (user_id,))
posts_count = cursor.fetchone()["count"]
# Get followers count
cursor.execute(
"SELECT COUNT(*) as count FROM follows WHERE following_id = ?",
(user_id,),
)
followers_count = cursor.fetchone()["count"]
# Get following count
cursor.execute(
"SELECT COUNT(*) as count FROM follows WHERE follower_id = ?",
(user_id,),
)
following_count = cursor.fetchone()["count"]
user["posts_count"] = posts_count
user["followers_count"] = followers_count
user["following_count"] = following_count
return user
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
conn: sqlite3.Connection = Depends(get_db),
) -> UserResponse:
"""Get user profile."""
user = _get_user_with_stats(conn, user_id)
return UserResponse(**user)
@router.get("/{user_id}/posts", response_model=List[PostResponse])
async def get_user_posts(
user_id: int,
conn: sqlite3.Connection = Depends(get_db),
) -> List[PostResponse]:
"""Get all posts by a user."""
cursor = conn.cursor()
# Check user exists
cursor.execute("SELECT id FROM users WHERE id = ?", (user_id,))
if not cursor.fetchone():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
# Get posts with counts
cursor.execute(
"""
SELECT
p.id, p.user_id, u.username, p.image_path, p.caption, p.created_at,
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as likes_count,
(SELECT COUNT(*) FROM dislikes WHERE post_id = p.id) as dislikes_count,
(SELECT COUNT(*) FROM comments WHERE post_id = p.id) as comments_count
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.user_id = ?
ORDER BY p.created_at DESC
""",
(user_id,),
)
posts = []
for row in cursor.fetchall():
post = row_to_dict(row)
posts.append(
PostResponse(
id=post["id"],
user_id=post["user_id"],
username=post["username"],
image_url=f"/uploads/{post['image_path'].split('/')[-1]}",
caption=post["caption"],
likes_count=post["likes_count"],
dislikes_count=post["dislikes_count"],
comments_count=post["comments_count"],
created_at=post["created_at"],
)
)
return posts
@router.get("/{user_id}/stats", response_model=UserStats)
async def get_user_stats(
user_id: int,
conn: sqlite3.Connection = Depends(get_db),
) -> UserStats:
"""Get user statistics."""
user = _get_user_with_stats(conn, user_id)
return UserStats(
posts_count=user["posts_count"],
followers_count=user["followers_count"],
following_count=user["following_count"],
)
@router.post("/{user_id}/follow", status_code=status.HTTP_201_CREATED)
async def follow_user(
user_id: int,
credentials: HTTPAuthorizationCredentials = Depends(security),
conn: sqlite3.Connection = Depends(get_db),
) -> dict:
"""Follow a user."""
current_user_id = await get_current_user_id(credentials)
cursor = conn.cursor()
# Check user exists
cursor.execute("SELECT id FROM users WHERE id = ?", (user_id,))
if not cursor.fetchone():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
# Cannot follow yourself
if current_user_id == user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You cannot follow yourself",
)
# Check if already following
cursor.execute(
"SELECT id FROM follows WHERE follower_id = ? AND following_id = ?",
(current_user_id, user_id),
)
if cursor.fetchone():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You already follow this user",
)
# Create follow
cursor.execute(
"INSERT INTO follows (follower_id, following_id) VALUES (?, ?)",
(current_user_id, user_id),
)
conn.commit()
return {"message": "Successfully followed user"}
@router.delete("/{user_id}/follow", status_code=status.HTTP_200_OK)
async def unfollow_user(
user_id: int,
credentials: HTTPAuthorizationCredentials = Depends(security),
conn: sqlite3.Connection = Depends(get_db),
) -> dict:
"""Unfollow a user."""
current_user_id = await get_current_user_id(credentials)
cursor = conn.cursor()
# Check if following
cursor.execute(
"SELECT id FROM follows WHERE follower_id = ? AND following_id = ?",
(current_user_id, user_id),
)
if not cursor.fetchone():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="You are not following this user",
)
# Delete follow
cursor.execute(
"DELETE FROM follows WHERE follower_id = ? AND following_id = ?",
(current_user_id, user_id),
)
conn.commit()
return {"message": "Successfully unfollowed user"}