Backend enhancements: auth, channels, restaurants, daemon improvements
- Add admin auth dependency and role checks - Expand channel and restaurant API routes - Improve YouTube transcript fetching - Enhance daemon worker with better error handling and scheduling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,5 +36,22 @@ def login_google(body: GoogleLoginRequest):
|
||||
|
||||
@router.get("/me")
|
||||
def get_me(current_user: dict = Depends(get_current_user)):
|
||||
"""Return current authenticated user info."""
|
||||
return current_user
|
||||
"""Return current authenticated user info including admin status."""
|
||||
from core.db import conn
|
||||
user_id = current_user.get("sub") or current_user.get("id")
|
||||
with conn() as c:
|
||||
cur = c.cursor()
|
||||
cur.execute(
|
||||
"SELECT id, email, nickname, avatar_url, is_admin FROM tasteby_users WHERE id = :id",
|
||||
{"id": user_id},
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "User not found")
|
||||
return {
|
||||
"id": row[0],
|
||||
"email": row[1],
|
||||
"nickname": row[2],
|
||||
"avatar_url": row[3],
|
||||
"is_admin": bool(row[4]),
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core import youtube
|
||||
from api.deps import get_admin_user
|
||||
|
||||
from core import youtube, cache
|
||||
|
||||
_executor = ThreadPoolExecutor(max_workers=4)
|
||||
|
||||
@@ -23,13 +25,20 @@ class ChannelCreate(BaseModel):
|
||||
|
||||
@router.get("")
|
||||
def list_channels():
|
||||
return youtube.get_active_channels()
|
||||
key = cache.make_key("channels")
|
||||
cached = cache.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
result = youtube.get_active_channels()
|
||||
cache.set(key, result)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_channel(body: ChannelCreate):
|
||||
def create_channel(body: ChannelCreate, _admin: dict = Depends(get_admin_user)):
|
||||
try:
|
||||
row_id = youtube.add_channel(body.channel_id, body.channel_name, body.title_filter)
|
||||
cache.flush()
|
||||
return {"id": row_id, "channel_id": body.channel_id}
|
||||
except Exception as e:
|
||||
if "UQ_CHANNELS_CID" in str(e).upper():
|
||||
@@ -63,12 +72,15 @@ def _do_scan(channel_id: str, full: bool):
|
||||
if not full and new_in_page == 0 and total_fetched > 50:
|
||||
break
|
||||
|
||||
filtered = total_fetched - len(candidates) - len([v for v in candidates if v["video_id"] in existing_vids])
|
||||
new_count = youtube.save_videos_batch(ch["id"], candidates)
|
||||
return {"total_fetched": total_fetched, "new_videos": new_count}
|
||||
if new_count > 0:
|
||||
cache.flush()
|
||||
return {"total_fetched": total_fetched, "new_videos": new_count, "filtered": filtered if title_filter else 0}
|
||||
|
||||
|
||||
@router.post("/{channel_id}/scan")
|
||||
async def scan_channel(channel_id: str, full: bool = False):
|
||||
async def scan_channel(channel_id: str, full: bool = False, _admin: dict = Depends(get_admin_user)):
|
||||
"""Trigger a scan for new videos from this channel (non-blocking)."""
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(_executor, _do_scan, channel_id, full)
|
||||
@@ -78,7 +90,7 @@ async def scan_channel(channel_id: str, full: bool = False):
|
||||
|
||||
|
||||
@router.delete("/{channel_id:path}")
|
||||
def delete_channel(channel_id: str):
|
||||
def delete_channel(channel_id: str, _admin: dict = Depends(get_admin_user)):
|
||||
"""Deactivate a channel. Accepts channel_id or DB id."""
|
||||
deleted = youtube.deactivate_channel(channel_id)
|
||||
if not deleted:
|
||||
@@ -86,4 +98,5 @@ def delete_channel(channel_id: str):
|
||||
deleted = youtube.deactivate_channel_by_db_id(channel_id)
|
||||
if not deleted:
|
||||
raise HTTPException(404, "Channel not found")
|
||||
cache.flush()
|
||||
return {"ok": True}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from core import restaurant
|
||||
from api.deps import get_admin_user
|
||||
from core import restaurant, cache
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -17,19 +18,30 @@ def list_restaurants(
|
||||
region: str | None = None,
|
||||
channel: str | None = None,
|
||||
):
|
||||
return restaurant.get_all(limit=limit, offset=offset, cuisine=cuisine, region=region, channel=channel)
|
||||
key = cache.make_key("restaurants", f"l={limit}", f"o={offset}", f"c={cuisine}", f"r={region}", f"ch={channel}")
|
||||
cached = cache.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
result = restaurant.get_all(limit=limit, offset=offset, cuisine=cuisine, region=region, channel=channel)
|
||||
cache.set(key, result)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{restaurant_id}")
|
||||
def get_restaurant(restaurant_id: str):
|
||||
key = cache.make_key("restaurant", restaurant_id)
|
||||
cached = cache.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
r = restaurant.get_by_id(restaurant_id)
|
||||
if not r:
|
||||
raise HTTPException(404, "Restaurant not found")
|
||||
cache.set(key, r)
|
||||
return r
|
||||
|
||||
|
||||
@router.put("/{restaurant_id}")
|
||||
def update_restaurant(restaurant_id: str, body: dict):
|
||||
def update_restaurant(restaurant_id: str, body: dict, _admin: dict = Depends(get_admin_user)):
|
||||
from core.db import conn
|
||||
r = restaurant.get_by_id(restaurant_id)
|
||||
if not r:
|
||||
@@ -49,11 +61,12 @@ def update_restaurant(restaurant_id: str, body: dict):
|
||||
sql = f"UPDATE restaurants SET {', '.join(sets)} WHERE id = :rid"
|
||||
with conn() as c:
|
||||
c.cursor().execute(sql, params)
|
||||
cache.flush()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/{restaurant_id}")
|
||||
def delete_restaurant(restaurant_id: str):
|
||||
def delete_restaurant(restaurant_id: str, _admin: dict = Depends(get_admin_user)):
|
||||
from core.db import conn
|
||||
r = restaurant.get_by_id(restaurant_id)
|
||||
if not r:
|
||||
@@ -64,12 +77,19 @@ def delete_restaurant(restaurant_id: str):
|
||||
cur.execute("DELETE FROM user_reviews WHERE restaurant_id = :rid", {"rid": restaurant_id})
|
||||
cur.execute("DELETE FROM video_restaurants WHERE restaurant_id = :rid", {"rid": restaurant_id})
|
||||
cur.execute("DELETE FROM restaurants WHERE id = :rid", {"rid": restaurant_id})
|
||||
cache.flush()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/{restaurant_id}/videos")
|
||||
def get_restaurant_videos(restaurant_id: str):
|
||||
key = cache.make_key("restaurant_videos", restaurant_id)
|
||||
cached = cache.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
r = restaurant.get_by_id(restaurant_id)
|
||||
if not r:
|
||||
raise HTTPException(404, "Restaurant not found")
|
||||
return restaurant.get_video_links(restaurant_id)
|
||||
result = restaurant.get_video_links(restaurant_id)
|
||||
cache.set(key, result)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user