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
02481b3283
commit
d02b540c60
8 changed files with 152 additions and 11 deletions
|
|
@ -9,6 +9,7 @@ import {
|
|||
Plus,
|
||||
RefreshCw,
|
||||
Search,
|
||||
UserMinus,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { api, type ManagedChannel, type Tag } from "../lib/api";
|
||||
|
|
@ -17,8 +18,10 @@ import { notify } from "../lib/notifications";
|
|||
import Tooltip from "./Tooltip";
|
||||
|
||||
export default function Channels({
|
||||
canWrite,
|
||||
onViewChannel,
|
||||
}: {
|
||||
canWrite: boolean;
|
||||
onViewChannel: (id: string, name: string) => void;
|
||||
}) {
|
||||
const qc = useQueryClient();
|
||||
|
|
@ -70,6 +73,15 @@ export default function Channels({
|
|||
mutationFn: (id: number) => api.deleteTag(id),
|
||||
onSuccess: () => invalidate(),
|
||||
});
|
||||
const unsubscribe = useMutation({
|
||||
mutationFn: (id: string) => api.unsubscribeChannel(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["channels"] });
|
||||
qc.invalidateQueries({ queryKey: ["my-status"] });
|
||||
notify({ level: "success", message: "Unsubscribed on YouTube" });
|
||||
},
|
||||
onError: () => notify({ level: "error", message: "Unsubscribe failed" }),
|
||||
});
|
||||
const deepAll = useMutation({
|
||||
mutationFn: () => api.deepAll(true),
|
||||
onSuccess: (r: { updated?: number }) => {
|
||||
|
|
@ -214,6 +226,15 @@ export default function Channels({
|
|||
key={c.id}
|
||||
c={c}
|
||||
userTags={userTags}
|
||||
canWrite={canWrite}
|
||||
onUnsubscribe={() => {
|
||||
if (
|
||||
window.confirm(
|
||||
`Unsubscribe from "${c.title ?? c.id}" on YouTube? This changes your real YouTube account. To just remove it from your feed, hide it instead.`
|
||||
)
|
||||
)
|
||||
unsubscribe.mutate(c.id);
|
||||
}}
|
||||
onView={() => onViewChannel(c.id, c.title ?? "This channel")}
|
||||
onPriority={(d) => patch.mutate({ id: c.id, body: { priority: c.priority + d } })}
|
||||
onHide={() => patch.mutate({ id: c.id, body: { hidden: !c.hidden } })}
|
||||
|
|
@ -268,6 +289,8 @@ function SyncBadge({ ok, label, hint }: { ok: boolean; label: string; hint?: str
|
|||
function ChannelRow({
|
||||
c,
|
||||
userTags,
|
||||
canWrite,
|
||||
onUnsubscribe,
|
||||
onView,
|
||||
onPriority,
|
||||
onHide,
|
||||
|
|
@ -276,6 +299,8 @@ function ChannelRow({
|
|||
}: {
|
||||
c: ManagedChannel;
|
||||
userTags: Tag[];
|
||||
canWrite: boolean;
|
||||
onUnsubscribe: () => void;
|
||||
onView: () => void;
|
||||
onPriority: (delta: number) => void;
|
||||
onHide: () => void;
|
||||
|
|
@ -373,6 +398,18 @@ function ChannelRow({
|
|||
{c.hidden ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{canWrite && (
|
||||
<Tooltip hint="Unsubscribe from this channel on YouTube (changes your real account). Read-only mode hides this — use Hide instead.">
|
||||
<button
|
||||
onClick={onUnsubscribe}
|
||||
className="text-muted hover:text-red-400 shrink-0"
|
||||
aria-label="Unsubscribe on YouTube"
|
||||
>
|
||||
<UserMinus className="w-4 h-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue