diff options
| author | Fuwn <[email protected]> | 2026-02-08 01:35:41 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-08 01:35:41 -0800 |
| commit | b8a9b40f786554e5a49511ce1b2dd2ea7f3db94c (patch) | |
| tree | 23fcdad5c10a7a269286459b4b666120d92f2af7 /apps | |
| parent | fix: update worker Dockerfile to Go 1.24 to match go.mod requirement (diff) | |
| download | asa.news-b8a9b40f786554e5a49511ce1b2dd2ea7f3db94c.tar.xz asa.news-b8a9b40f786554e5a49511ce1b2dd2ea7f3db94c.zip | |
feat: implement authenticated feed support across worker and web app
Wire up the full authenticated feeds pipeline:
- Worker resolves credentials from Supabase Vault for authenticated feeds
- Worker sets owner_id on entries for per-user dedup
- query_param auth now parses name=value format
- Add-feed dialog shows auth type + credential fields for pro/developer
- Subscribe mutation passes credentials to RPC
- Sidebar and settings show [auth] indicator for authenticated feeds
Diffstat (limited to 'apps')
6 files changed, 66 insertions, 1 deletions
diff --git a/apps/web/app/reader/_components/add-feed-dialog.tsx b/apps/web/app/reader/_components/add-feed-dialog.tsx index ff3e916..4ffbd39 100644 --- a/apps/web/app/reader/_components/add-feed-dialog.tsx +++ b/apps/web/app/reader/_components/add-feed-dialog.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from "react" import { useSubscribeToFeed } from "@/lib/queries/use-subscribe-to-feed" import { useSubscriptions } from "@/lib/queries/use-subscriptions" +import { useUserProfile } from "@/lib/queries/use-user-profile" import { useUserInterfaceStore } from "@/lib/stores/user-interface-store" export function AddFeedDialog() { @@ -13,13 +14,21 @@ export function AddFeedDialog() { const [selectedFolderIdentifier, setSelectedFolderIdentifier] = useState< string | null >(null) + const [authenticationType, setAuthenticationType] = useState("none") + const [feedCredential, setFeedCredential] = useState("") const subscribeToFeed = useSubscribeToFeed() const { data: subscriptionsData } = useSubscriptions() + const { data: userProfile } = useUserProfile() + + const supportsAuthenticatedFeeds = + userProfile?.tier === "pro" || userProfile?.tier === "developer" function handleClose() { setFeedUrl("") setCustomTitle("") setSelectedFolderIdentifier(null) + setAuthenticationType("none") + setFeedCredential("") setOpen(false) } @@ -31,6 +40,10 @@ export function AddFeedDialog() { feedUrl, folderIdentifier: selectedFolderIdentifier, customTitle: customTitle || null, + feedCredential: + authenticationType !== "none" ? feedCredential : null, + feedAuthenticationType: + authenticationType !== "none" ? authenticationType : null, }, { onSuccess: () => { @@ -124,6 +137,42 @@ export function AddFeedDialog() { ))} </select> </div> + {supportsAuthenticatedFeeds && ( + <div className="space-y-2"> + <label + htmlFor="auth-type" + className="text-text-secondary" + > + authentication (optional) + </label> + <select + id="auth-type" + value={authenticationType} + onChange={(event) => setAuthenticationType(event.target.value)} + className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none" + > + <option value="none">none</option> + <option value="bearer">bearer token</option> + <option value="basic">basic (user:pass)</option> + <option value="query_param">query parameter</option> + </select> + {authenticationType !== "none" && ( + <input + type="password" + value={feedCredential} + onChange={(event) => setFeedCredential(event.target.value)} + placeholder={ + authenticationType === "query_param" + ? "api_key=your_token" + : authenticationType === "basic" + ? "username:password" + : "your token" + } + className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim" + /> + )} + </div> + )} <div className="flex gap-2"> <button type="button" diff --git a/apps/web/app/reader/_components/sidebar-content.tsx b/apps/web/app/reader/_components/sidebar-content.tsx index 401d203..e43451d 100644 --- a/apps/web/app/reader/_components/sidebar-content.tsx +++ b/apps/web/app/reader/_components/sidebar-content.tsx @@ -295,6 +295,9 @@ export function SidebarContent() { <span className={classNames("truncate", showFeedFavicons && "ml-2")}> {displayNameForSubscription(subscription)} </span> + {subscription.feedVisibility === "authenticated" && ( + <span className="ml-1 shrink-0 text-text-dim" title="authenticated feed">🔒</span> + )} {subscription.feedType === "podcast" && ( <span className="ml-1 shrink-0 text-text-dim" title="podcast">♫</span> )} @@ -338,6 +341,9 @@ export function SidebarContent() { <span className={classNames("truncate", showFeedFavicons && "ml-2")}> {displayNameForSubscription(subscription)} </span> + {subscription.feedVisibility === "authenticated" && ( + <span className="ml-1 shrink-0 text-text-dim" title="authenticated feed">🔒</span> + )} {subscription.feedType === "podcast" && ( <span className="ml-1 shrink-0 text-text-dim" title="podcast">♫</span> )} diff --git a/apps/web/app/reader/settings/_components/subscriptions-settings.tsx b/apps/web/app/reader/settings/_components/subscriptions-settings.tsx index 2c79238..e22a005 100644 --- a/apps/web/app/reader/settings/_components/subscriptions-settings.tsx +++ b/apps/web/app/reader/settings/_components/subscriptions-settings.tsx @@ -124,6 +124,9 @@ function SubscriptionRow({ <span className="truncate text-text-primary"> {subscription.customTitle ?? subscription.feedTitle} </span> + {subscription.feedVisibility === "authenticated" && ( + <span className="shrink-0 text-text-dim">[auth]</span> + )} <button onClick={() => { setEditedTitle(subscription.customTitle ?? "") diff --git a/apps/web/lib/queries/use-subscribe-to-feed.ts b/apps/web/lib/queries/use-subscribe-to-feed.ts index 5e585a9..ead1d39 100644 --- a/apps/web/lib/queries/use-subscribe-to-feed.ts +++ b/apps/web/lib/queries/use-subscribe-to-feed.ts @@ -14,11 +14,15 @@ export function useSubscribeToFeed() { feedUrl: string folderIdentifier?: string | null customTitle?: string | null + feedCredential?: string | null + feedAuthenticationType?: string | null }) => { const { data, error } = await supabaseClient.rpc("subscribe_to_feed", { feed_url: parameters.feedUrl, target_folder_id: parameters.folderIdentifier ?? undefined, feed_custom_title: parameters.customTitle ?? undefined, + feed_credential: parameters.feedCredential ?? undefined, + feed_auth_type: parameters.feedAuthenticationType ?? undefined, }) if (error) throw error diff --git a/apps/web/lib/queries/use-subscriptions.ts b/apps/web/lib/queries/use-subscriptions.ts index e6b84ef..2378411 100644 --- a/apps/web/lib/queries/use-subscriptions.ts +++ b/apps/web/lib/queries/use-subscriptions.ts @@ -14,6 +14,7 @@ interface SubscriptionRow { feeds: { title: string | null url: string + visibility: "public" | "authenticated" consecutive_failures: number last_fetch_error: string | null last_fetched_at: string | null @@ -38,7 +39,7 @@ export function useSubscriptions() { const [subscriptionsResult, foldersResult] = await Promise.all([ supabaseClient .from("subscriptions") - .select("id, feed_id, folder_id, custom_title, position, feeds(title, url, consecutive_failures, last_fetch_error, last_fetched_at, fetch_interval_seconds, feed_type)") + .select("id, feed_id, folder_id, custom_title, position, feeds(title, url, visibility, consecutive_failures, last_fetch_error, last_fetched_at, fetch_interval_seconds, feed_type)") .order("position", { ascending: true }), supabaseClient .from("folders") @@ -63,6 +64,7 @@ export function useSubscriptions() { lastFetchedAt: row.feeds?.last_fetched_at ?? null, fetchIntervalSeconds: row.feeds?.fetch_interval_seconds ?? 3600, feedType: row.feeds?.feed_type ?? null, + feedVisibility: row.feeds?.visibility ?? "public", })) const folders: Folder[] = ( diff --git a/apps/web/lib/types/subscription.ts b/apps/web/lib/types/subscription.ts index f2ba995..0dbc8cb 100644 --- a/apps/web/lib/types/subscription.ts +++ b/apps/web/lib/types/subscription.ts @@ -17,4 +17,5 @@ export interface Subscription { lastFetchedAt: string | null fetchIntervalSeconds: number feedType: string | null + feedVisibility: "public" | "authenticated" } |