diff options
| author | Fuwn <[email protected]> | 2026-02-09 23:48:27 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-09 23:48:27 -0800 |
| commit | d0d49d9b759f841c0a05b2410efbd26957a813dc (patch) | |
| tree | 6c72bd6a27e13b6cadb2d1797afedc172ffc32e7 /apps/web | |
| parent | fix: P0 correctness and security fixes (diff) | |
| download | asa.news-d0d49d9b759f841c0a05b2410efbd26957a813dc.tar.xz asa.news-d0d49d9b759f841c0a05b2410efbd26957a813dc.zip | |
fix: P0 correctness/security fixes and P1 lint error resolution
P0: add missing 'developer' case to check_custom_feed_limit trigger,
scope user_entry_states join to authenticated user in API v1 entries,
replace in-memory rate limiting with Supabase-backed check_rate_limit RPC.
P1: fix all 9 ESLint errors — useSyncExternalStore for useIsMobile,
restructure WebhookSection to avoid set-state-in-effect, move ref
mutations into useEffect, replace <a> with <Link> on shared page,
ignore generated public/sw.js in eslint config.
Diffstat (limited to 'apps/web')
| -rw-r--r-- | apps/web/app/reader/_components/entry-detail-panel.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/reader/_components/reader-layout-shell.tsx | 10 | ||||
| -rw-r--r-- | apps/web/app/reader/_components/reader-shell.tsx | 5 | ||||
| -rw-r--r-- | apps/web/app/reader/_components/search-overlay.tsx | 9 | ||||
| -rw-r--r-- | apps/web/app/reader/settings/_components/api-settings.tsx | 44 | ||||
| -rw-r--r-- | apps/web/app/shared/[token]/page.tsx | 5 | ||||
| -rw-r--r-- | apps/web/eslint.config.mjs | 1 | ||||
| -rw-r--r-- | apps/web/lib/hooks/use-is-mobile.ts | 32 |
8 files changed, 63 insertions, 45 deletions
diff --git a/apps/web/app/reader/_components/entry-detail-panel.tsx b/apps/web/app/reader/_components/entry-detail-panel.tsx index 6982083..9848d3f 100644 --- a/apps/web/app/reader/_components/entry-detail-panel.tsx +++ b/apps/web/app/reader/_components/entry-detail-panel.tsx @@ -186,7 +186,7 @@ export function EntryDetailPanel({ } } - setUnpositionedHighlights(failedHighlights) + queueMicrotask(() => setUnpositionedHighlights(failedHighlights)) }, [sanitisedContent, highlightsData]) const handleTextSelection = useCallback(() => { diff --git a/apps/web/app/reader/_components/reader-layout-shell.tsx b/apps/web/app/reader/_components/reader-layout-shell.tsx index fe158b5..391e6a4 100644 --- a/apps/web/app/reader/_components/reader-layout-shell.tsx +++ b/apps/web/app/reader/_components/reader-layout-shell.tsx @@ -162,9 +162,15 @@ export function ReaderLayoutShell({ const sidebarGroupRef = useGroupRef() const sidebarMaxWidthRef = useRef(sidebarMaxWidth) - sidebarMaxWidthRef.current = sidebarMaxWidth const sidebarOnLayoutChangedRef = useRef(sidebarLayout.onLayoutChanged) - sidebarOnLayoutChangedRef.current = sidebarLayout.onLayoutChanged + + useEffect(() => { + sidebarMaxWidthRef.current = sidebarMaxWidth + }, [sidebarMaxWidth]) + + useEffect(() => { + sidebarOnLayoutChangedRef.current = sidebarLayout.onLayoutChanged + }, [sidebarLayout.onLayoutChanged]) useEffect(() => { useUserInterfaceStore.getState().setResetSidebarLayout(() => { diff --git a/apps/web/app/reader/_components/reader-shell.tsx b/apps/web/app/reader/_components/reader-shell.tsx index 8a5a044..65a4900 100644 --- a/apps/web/app/reader/_components/reader-shell.tsx +++ b/apps/web/app/reader/_components/reader-shell.tsx @@ -67,7 +67,10 @@ export function ReaderShell({ const detailGroupRef = useGroupRef() const detailOnLayoutChangedRef = useRef(detailLayout.onLayoutChanged) - detailOnLayoutChangedRef.current = detailLayout.onLayoutChanged + + useEffect(() => { + detailOnLayoutChangedRef.current = detailLayout.onLayoutChanged + }, [detailLayout.onLayoutChanged]) useEffect(() => { useUserInterfaceStore.getState().setResetDetailLayout(() => { diff --git a/apps/web/app/reader/_components/search-overlay.tsx b/apps/web/app/reader/_components/search-overlay.tsx index 5cfdb57..11e709c 100644 --- a/apps/web/app/reader/_components/search-overlay.tsx +++ b/apps/web/app/reader/_components/search-overlay.tsx @@ -50,10 +50,6 @@ export function SearchOverlay({ onClose }: SearchOverlayProperties) { }, []) useEffect(() => { - setSelectedResultIndex(-1) - }, [searchQuery]) - - useEffect(() => { function handleKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { onClose() @@ -122,7 +118,10 @@ export function SearchOverlay({ onClose }: SearchOverlayProperties) { ref={inputReference} type="text" value={searchQuery} - onChange={(event) => setSearchQuery(event.target.value)} + onChange={(event) => { + setSearchQuery(event.target.value) + setSelectedResultIndex(-1) + }} onKeyDown={handleInputKeyDown} placeholder="search entries..." className="w-full bg-transparent text-text-primary outline-none placeholder:text-text-dim" diff --git a/apps/web/app/reader/settings/_components/api-settings.tsx b/apps/web/app/reader/settings/_components/api-settings.tsx index cca673f..102e3fe 100644 --- a/apps/web/app/reader/settings/_components/api-settings.tsx +++ b/apps/web/app/reader/settings/_components/api-settings.tsx @@ -1,9 +1,8 @@ "use client" -import { useState, useEffect } from "react" +import { useState } from "react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useUserProfile } from "@/lib/queries/use-user-profile" -import { queryKeys } from "@/lib/queries/query-keys" import { notify } from "@/lib/notify" interface ApiKey { @@ -293,21 +292,36 @@ function ApiKeysSection() { ) } -function WebhookSection() { +function WebhookSectionLoading() { const { data: webhookConfig, isLoading } = useWebhookConfig() + + if (isLoading) { + return <p className="text-text-dim">loading webhook configuration ...</p> + } + + if (!webhookConfig) { + return <p className="text-text-dim">failed to load webhook configuration</p> + } + + return <WebhookSection initialConfiguration={webhookConfig} /> +} + +function WebhookSection({ + initialConfiguration, +}: { + initialConfiguration: WebhookConfiguration +}) { + const { data: webhookConfig } = useWebhookConfig() const updateWebhookConfig = useUpdateWebhookConfig() const testWebhook = useTestWebhook() - const [webhookUrl, setWebhookUrl] = useState("") - const [webhookSecret, setWebhookSecret] = useState("") + const [webhookUrl, setWebhookUrl] = useState( + initialConfiguration.webhookUrl ?? "" + ) + const [webhookSecret, setWebhookSecret] = useState( + initialConfiguration.webhookSecret ?? "" + ) const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) - useEffect(() => { - if (webhookConfig) { - setWebhookUrl(webhookConfig.webhookUrl ?? "") - setWebhookSecret(webhookConfig.webhookSecret ?? "") - } - }, [webhookConfig]) - function handleSaveWebhookConfig() { updateWebhookConfig.mutate( { @@ -371,10 +385,6 @@ function WebhookSection() { setHasUnsavedChanges(true) } - if (isLoading) { - return <p className="text-text-dim">loading webhook configuration ...</p> - } - return ( <div className="mb-6"> <h3 className="mb-2 text-text-primary">webhooks</h3> @@ -503,7 +513,7 @@ export function ApiSettings() { return ( <div className="px-4 py-3"> <ApiKeysSection /> - <WebhookSection /> + <WebhookSectionLoading /> <div> <h3 className="mb-2 text-text-primary">api documentation</h3> diff --git a/apps/web/app/shared/[token]/page.tsx b/apps/web/app/shared/[token]/page.tsx index 7c7a463..eaab4f1 100644 --- a/apps/web/app/shared/[token]/page.tsx +++ b/apps/web/app/shared/[token]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next" +import Link from "next/link" import { createSupabaseAdminClient } from "@/lib/supabase/admin" import { sanitizeEntryContent } from "@/lib/sanitize" import { SharedEntryContent } from "./shared-entry-content" @@ -159,12 +160,12 @@ export default async function SharedPage({ params }: SharedPageProperties) { <footer className="mt-12 border-t border-border pt-4 text-text-dim"> <p> shared from{" "} - <a + <Link href="/" className="text-text-secondary transition-colors hover:text-text-primary" > asa.news - </a> + </Link> </p> {entry.url && ( <p className="mt-1"> diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs index 7bf5713..ceb3b01 100644 --- a/apps/web/eslint.config.mjs +++ b/apps/web/eslint.config.mjs @@ -12,6 +12,7 @@ const eslintConfig = defineConfig([ "out/**", "build/**", "next-env.d.ts", + "public/sw.js", ]), { plugins: { diff --git a/apps/web/lib/hooks/use-is-mobile.ts b/apps/web/lib/hooks/use-is-mobile.ts index a56e36c..0074ef5 100644 --- a/apps/web/lib/hooks/use-is-mobile.ts +++ b/apps/web/lib/hooks/use-is-mobile.ts @@ -1,26 +1,24 @@ "use client" -import { useState, useEffect } from "react" +import { useSyncExternalStore } from "react" const MOBILE_BREAKPOINT = 768 +const MEDIA_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)` -export function useIsMobile(): boolean { - const [isMobile, setIsMobile] = useState(false) - - useEffect(() => { - const mediaQuery = window.matchMedia( - `(max-width: ${MOBILE_BREAKPOINT - 1}px)` - ) - - setIsMobile(mediaQuery.matches) +function subscribe(callback: () => void) { + const mediaQueryList = window.matchMedia(MEDIA_QUERY) + mediaQueryList.addEventListener("change", callback) + return () => mediaQueryList.removeEventListener("change", callback) +} - function handleChange(event: MediaQueryListEvent) { - setIsMobile(event.matches) - } +function getSnapshot() { + return window.matchMedia(MEDIA_QUERY).matches +} - mediaQuery.addEventListener("change", handleChange) - return () => mediaQuery.removeEventListener("change", handleChange) - }, []) +function getServerSnapshot() { + return false +} - return isMobile +export function useIsMobile(): boolean { + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) } |