feat(m5b): optional YouTube write scope via incremental OAuth
Default login now requests read-only (youtube.readonly); write (unsubscribe,
later playlist export) is an explicit opt-in.
- auth.py: split READ_SCOPES / WRITE_SCOPES; new GET /auth/upgrade (incremental
consent, prompt=consent); has_write_scope() helper
- /api/me exposes can_write
- youtube/client.py: delete_subscription (50 units, OAuth-only)
- DELETE /api/channels/{id}/subscription, gated on write scope (403 otherwise)
- UI: Settings - Account 'Playlist editing & YouTube export' enable button;
per-channel 'Unsubscribe on YouTube' (with confirm) shown only when can_write
Browser-facing; develop/test locally until the public HTTPS login lands. Needs a
one-time Console step: add youtube.readonly to the OAuth consent screen scopes.
This commit is contained in:
parent
f4b8a721fb
commit
43c05ea14b
8 changed files with 152 additions and 11 deletions
|
|
@ -5,9 +5,10 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth import current_user
|
||||
from app.auth import current_user, has_write_scope
|
||||
from app.db import get_db
|
||||
from app.models import Channel, ChannelTag, Subscription, Tag, User, Video
|
||||
from app.youtube.client import YouTubeClient, YouTubeError
|
||||
|
||||
router = APIRouter(prefix="/api/channels", tags=["channels"])
|
||||
|
||||
|
|
@ -101,6 +102,35 @@ def update_channel(
|
|||
}
|
||||
|
||||
|
||||
@router.delete("/{channel_id}/subscription")
|
||||
def unsubscribe(
|
||||
channel_id: str,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Unsubscribe from this channel on YouTube and drop the local subscription. Gated
|
||||
behind the optional write scope; read-only users get a 403 and should Hide instead."""
|
||||
if not has_write_scope(user):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Enable playlist editing in Settings to unsubscribe on YouTube.",
|
||||
)
|
||||
sub = _user_subscription(db, user, channel_id)
|
||||
if not sub.yt_subscription_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No YouTube subscription id on record — sync subscriptions first.",
|
||||
)
|
||||
try:
|
||||
with YouTubeClient(db, user) as yt:
|
||||
yt.delete_subscription(sub.yt_subscription_id)
|
||||
except YouTubeError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"YouTube unsubscribe failed: {exc}")
|
||||
db.delete(sub)
|
||||
db.commit()
|
||||
return {"unsubscribed": channel_id}
|
||||
|
||||
|
||||
@router.post("/{channel_id}/tags")
|
||||
def attach_tag(
|
||||
channel_id: str,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue