Skip to content

Let the signup route's trial row win over the trigger's - #236

Merged
ralyodio merged 1 commit into
masterfrom
fix/signup-ip-upsert-wins
Sep 26, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix/signup-ip-upsert-wins

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Problem

on_auth_user_created_subscription (create_trial_subscription()) inserts a plain 3-day user_subscriptions row the moment a user is created. The signup route then upserted its own row with ignoreDuplicates: true, so its row never landed: signup_ip stayed NULL on every user since the anti-abuse feature shipped (2026-02-15), and the repeat-IP 1-day trial never applied. Prod today: 2,163 rows, 0 with a trial shorter than 3 days.

Change

  • src/app/api/auth/signup/route.ts: upsert with merge (ignoreDuplicates: false, i.e. ON CONFLICT (user_id) DO UPDATE) so the route's signup_ip, trial_started_at and trial_expires_at land on the trigger's row. Columns the route does not send (id, created_at, renewal flags, subscription_*) are left as the trigger/defaults set them.
  • The trigger and migrations are untouched; the trigger still covers OAuth and magic-link signups that never reach this route.

Tests

src/app/api/auth/signup/route.test.ts gains a Trial anti-abuse (signup_ip) block:

  • fresh IP: 201, lookup by first X-Forwarded-For hop excluding the user's own row, upsert payload has signup_ip set and a 3-day window, options { onConflict: 'user_id', ignoreDuplicates: false }
  • repeat IP: same, with a 1-day window

Both fail against the old route and pass with this one. Full CI gates run locally: eslint 0 errors, tsc --noEmit clean, vitest 2,921 passed / 219 files.

Deploy

Merging to master runs "CI Pipeline", and deploy-droplet.yml chains off its success (workflow_run), so no dispatch is needed. Verification on the droplet after deploy: /api/health healthy, a throwaway signup from a test IP writes signup_ip and a 3-day trial_expires_at.

🤖 Generated with Claude Code

The on_auth_user_created_subscription trigger inserts a plain 3-day
user_subscriptions row the instant auth.users gets a row, so by the time
signUp() returns the row already exists. The route then upserted its own
row with ignoreDuplicates: true, which turned the upsert into a no-op:
signup_ip stayed NULL on every user and the repeat-IP 1-day trial never
applied.

Upsert with merge instead (ON CONFLICT (user_id) DO UPDATE), so the
route's signup_ip and trial window land on the trigger's row. Columns the
route does not send (id, created_at, renewal flags, subscription_*) are
untouched. The trigger stays, since it covers OAuth and magic-link
signups that never hit this route.

Tests cover both branches: a fresh IP gets 3 days with signup_ip set, a
repeat IP gets 1 day, and both assert the upsert merges rather than
ignores duplicates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

95 finding(s)

HIGH/CRITICAL: 11 | MEDIUM: 29 | LOW: 55

Severity Rule Location
HIGH secret-private-key src/app/settings/seedbox-section.tsx:412
HIGH secret-generic-api-key docs/incidents/2026-05-okshanaby-supply-chain.md:18
HIGH tls-verification-disabled src/app/api/iptv-proxy/route.ts:38
HIGH tls-verification-disabled src/app/api/iptv/channels/route.ts:35
HIGH tls-verification-disabled src/app/api/iptv/playlists/[id]/route.ts:73
HIGH tls-verification-disabled src/app/api/iptv/playlists/route.ts:64
HIGH js-cors-origin-reflected src/app/api/public/shares/[slug]/checkout/route.ts:40
HIGH js-cors-origin-reflected src/app/api/public/vod/[slug]/checkout/route.ts:34
HIGH tls-verification-disabled src/lib/iptv/shares/upstream.ts:37
HIGH tls-verification-disabled workers/iptv-cache/epg-fetcher.ts:25
HIGH tls-verification-disabled workers/iptv-cache/playlist-fetcher.ts:62
MEDIUM secret-jwt .github/workflows/ci.yml:120
MEDIUM secret-jwt .github/workflows/ci.yml:121
MEDIUM secret-jwt .github/workflows/ci.yml:123
MEDIUM secret-jwt .github/workflows/ci.yml:162
MEDIUM secret-jwt .github/workflows/ci.yml:164
MEDIUM secret-jwt docs/tunein (2).py:9
MEDIUM secret-jwt docs/tunein.py:9
MEDIUM sh-remote-script-execution scripts/setup-server.sh:182
MEDIUM sh-remote-script-execution scripts/setup-server.sh:419
MEDIUM sh-remote-script-execution scripts/setup-server.sh:428
MEDIUM sh-unquoted-expansion-destructive scripts/setup-server.sh:1096
MEDIUM sh-unquoted-expansion-destructive scripts/setup-server.sh:1106
MEDIUM js-unescaped-html-sink src/app/api/player/route.ts:110
MEDIUM js-unescaped-html-sink src/app/api/player/route.ts:249
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:40
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:66
MEDIUM js-unescaped-html-sink src/app/email/email-content.tsx:566
MEDIUM js-open-redirect src/app/login/page.tsx:68
MEDIUM js-open-redirect src/app/pricing/page.tsx:162
MEDIUM js-open-redirect src/app/rent/[slug]/rent-client.tsx:170
MEDIUM js-unescaped-html-sink src/app/rss/rss-content.tsx:615
MEDIUM js-open-redirect src/app/vod/[slug]/vod-client.tsx:134
MEDIUM js-open-redirect src/app/watch/[slug]/watch-client.tsx:129
MEDIUM js-unescaped-html-sink src/app/youtube/youtube-content.tsx:546
MEDIUM js-open-redirect src/components/account/iptv-subscription-section.tsx:135
MEDIUM js-open-redirect src/components/account/iptv-subscription-section.tsx:167
MEDIUM js-unescaped-html-sink src/components/news/news-section.tsx:361
MEDIUM js-unescaped-html-sink src/components/news/news-section.tsx:734
MEDIUM redos-nested-quantifier src/lib/metadata-enrichment/metadata-enrichment.ts:317
LOW tls-verification-disabled docs/tunein (2).py:34
LOW tls-verification-disabled docs/tunein (2).py:37
LOW tls-verification-disabled docs/tunein (2).py:47
LOW tls-verification-disabled docs/tunein.py:34
LOW tls-verification-disabled docs/tunein.py:37
LOW tls-verification-disabled docs/tunein.py:47
LOW tls-verification-disabled src/app/api/iptv-proxy/route.test.ts:457
LOW secret-generic-credential src/app/api/iptv/subscription/route.test.ts:66
LOW secret-generic-credential src/lib/argontv/client.test.ts:54
LOW secret-generic-credential src/lib/argontv/client.test.ts:56

…and 45 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 41f65ed into master Sep 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant