summaryrefslogtreecommitdiff
path: root/apps/web/app
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 05:41:07 -0800
committerFuwn <[email protected]>2026-02-07 05:41:07 -0800
commita1a405e56a0907ed44bfaba721e0ea632e051141 (patch)
tree99621f7c1407eed732eeb5742fc7458c14ee6d48 /apps/web/app
parentsecurity: remove unsafe-eval CSP, fix host header injection, harden API routes (diff)
downloadasa.news-a1a405e56a0907ed44bfaba721e0ea632e051141.tar.xz
asa.news-a1a405e56a0907ed44bfaba721e0ea632e051141.zip
fix: resolve 6 pre-ship audit bugs
- Webhook entry identifier: use entry GUID instead of feed identifier - Optimistic rollback: add previousTimeline snapshot and onError handler to both useToggleEntryReadState and useToggleEntrySavedState - Rate limiter memory leak: delete Map entries when window expires, use else-if to avoid re-setting after delete - Entries API limit param: use Number.isFinite guard instead of falsy coercion that treats 0 as default - PWA manifest: add PNG raster icon routes (192x192, 512x512) for devices that don't support SVG icons - Billing webhook: throw on DB errors and return 500 so Stripe retries failed events instead of silently losing them
Diffstat (limited to 'apps/web/app')
-rw-r--r--apps/web/app/api/billing/webhook/route.ts60
-rw-r--r--apps/web/app/api/v1/entries/route.ts3
-rw-r--r--apps/web/app/icon-192x192/route.tsx25
-rw-r--r--apps/web/app/icon-512x512/route.tsx25
-rw-r--r--apps/web/app/manifest.ts15
5 files changed, 104 insertions, 24 deletions
diff --git a/apps/web/app/api/billing/webhook/route.ts b/apps/web/app/api/billing/webhook/route.ts
index 297ef8e..52b1420 100644
--- a/apps/web/app/api/billing/webhook/route.ts
+++ b/apps/web/app/api/billing/webhook/route.ts
@@ -45,6 +45,7 @@ async function updateBillingState(
if (error) {
console.error("failed to update billing state:", error)
+ throw new Error(`failed to update billing state: ${error.message}`)
}
}
@@ -62,7 +63,7 @@ async function handleCheckoutSessionCompleted(
)
const adminClient = createSupabaseAdminClient()
- await adminClient
+ const { error } = await adminClient
.from("user_profiles")
.update({
tier: determineTierFromSubscription(subscription),
@@ -72,6 +73,11 @@ async function handleCheckoutSessionCompleted(
stripe_current_period_end: extractPeriodEnd(subscription),
})
.eq("id", userIdentifier)
+
+ if (error) {
+ console.error("failed to update billing state on checkout:", error)
+ throw new Error(`failed to update billing state on checkout: ${error.message}`)
+ }
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
@@ -153,28 +159,36 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "invalid signature" }, { status: 400 })
}
- switch (event.type) {
- case "checkout.session.completed":
- await handleCheckoutSessionCompleted(
- event.data.object as Stripe.Checkout.Session
- )
- break
- case "customer.subscription.updated":
- await handleSubscriptionUpdated(
- event.data.object as Stripe.Subscription
- )
- break
- case "customer.subscription.deleted":
- await handleSubscriptionDeleted(
- event.data.object as Stripe.Subscription
- )
- break
- case "invoice.payment_failed":
- await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice)
- break
- case "invoice.paid":
- await handleInvoicePaid(event.data.object as Stripe.Invoice)
- break
+ try {
+ switch (event.type) {
+ case "checkout.session.completed":
+ await handleCheckoutSessionCompleted(
+ event.data.object as Stripe.Checkout.Session
+ )
+ break
+ case "customer.subscription.updated":
+ await handleSubscriptionUpdated(
+ event.data.object as Stripe.Subscription
+ )
+ break
+ case "customer.subscription.deleted":
+ await handleSubscriptionDeleted(
+ event.data.object as Stripe.Subscription
+ )
+ break
+ case "invoice.payment_failed":
+ await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice)
+ break
+ case "invoice.paid":
+ await handleInvoicePaid(event.data.object as Stripe.Invoice)
+ break
+ }
+ } catch (handlerError) {
+ console.error("webhook handler failed:", handlerError)
+ return NextResponse.json(
+ { error: "webhook handler failed" },
+ { status: 500 }
+ )
}
return NextResponse.json({ received: true })
diff --git a/apps/web/app/api/v1/entries/route.ts b/apps/web/app/api/v1/entries/route.ts
index e782e3b..8a2de62 100644
--- a/apps/web/app/api/v1/entries/route.ts
+++ b/apps/web/app/api/v1/entries/route.ts
@@ -18,7 +18,8 @@ export async function GET(request: Request) {
const isSaved = searchParams.get("isSaved")
const cursor = searchParams.get("cursor")
const limitParameter = searchParams.get("limit")
- const limit = Math.min(Math.max(Number(limitParameter) || 50, 1), 100)
+ const parsedLimit = Number(limitParameter)
+ const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(Math.floor(parsedLimit), 100) : 50
const adminClient = createSupabaseAdminClient()
diff --git a/apps/web/app/icon-192x192/route.tsx b/apps/web/app/icon-192x192/route.tsx
new file mode 100644
index 0000000..0a3d703
--- /dev/null
+++ b/apps/web/app/icon-192x192/route.tsx
@@ -0,0 +1,25 @@
+import { ImageResponse } from "next/og"
+
+export function GET() {
+ return new ImageResponse(
+ (
+ <div
+ style={{
+ width: "100%",
+ height: "100%",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#0a0a0a",
+ color: "#e5e5e5",
+ fontFamily: "monospace",
+ fontWeight: 700,
+ fontSize: 96,
+ }}
+ >
+ asa
+ </div>
+ ),
+ { width: 192, height: 192 }
+ )
+}
diff --git a/apps/web/app/icon-512x512/route.tsx b/apps/web/app/icon-512x512/route.tsx
new file mode 100644
index 0000000..fedee34
--- /dev/null
+++ b/apps/web/app/icon-512x512/route.tsx
@@ -0,0 +1,25 @@
+import { ImageResponse } from "next/og"
+
+export function GET() {
+ return new ImageResponse(
+ (
+ <div
+ style={{
+ width: "100%",
+ height: "100%",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#0a0a0a",
+ color: "#e5e5e5",
+ fontFamily: "monospace",
+ fontWeight: 700,
+ fontSize: 256,
+ }}
+ >
+ asa
+ </div>
+ ),
+ { width: 512, height: 512 }
+ )
+}
diff --git a/apps/web/app/manifest.ts b/apps/web/app/manifest.ts
index cb940d5..be2bc21 100644
--- a/apps/web/app/manifest.ts
+++ b/apps/web/app/manifest.ts
@@ -21,6 +21,21 @@ export default function manifest(): MetadataRoute.Manifest {
type: "image/svg+xml",
purpose: "maskable",
},
+ {
+ src: "/icon-192x192",
+ sizes: "192x192",
+ type: "image/png",
+ },
+ {
+ src: "/icon-512x512",
+ sizes: "512x512",
+ type: "image/png",
+ },
+ {
+ src: "/apple-icon",
+ sizes: "180x180",
+ type: "image/png",
+ },
],
}
}