summaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-09 23:48:27 -0800
committerFuwn <[email protected]>2026-02-09 23:48:27 -0800
commitd0d49d9b759f841c0a05b2410efbd26957a813dc (patch)
tree6c72bd6a27e13b6cadb2d1797afedc172ffc32e7 /apps/web
parentfix: P0 correctness and security fixes (diff)
downloadasa.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.tsx2
-rw-r--r--apps/web/app/reader/_components/reader-layout-shell.tsx10
-rw-r--r--apps/web/app/reader/_components/reader-shell.tsx5
-rw-r--r--apps/web/app/reader/_components/search-overlay.tsx9
-rw-r--r--apps/web/app/reader/settings/_components/api-settings.tsx44
-rw-r--r--apps/web/app/shared/[token]/page.tsx5
-rw-r--r--apps/web/eslint.config.mjs1
-rw-r--r--apps/web/lib/hooks/use-is-mobile.ts32
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)
}