feat(domains): dual-domain cutover to app.accounted.se (#1087)
* feat(domains): dual-domain cutover to app.accounted.se The user-facing app moves to app.accounted.se while app.gnubok.se stays alive for machine traffic (MCP connectors, API keys, third-party OAuth callbacks, webhooks, crons), so no third-party callback registration is on the critical path. - next.config: host redirect app.gnubok.se -> NEXT_PUBLIC_APP_URL for page traffic only (/api, /.well-known, /_next excluded). Arms itself only once NEXT_PUBLIC_APP_URL leaves the legacy host, so merging this is inert and the cutover is a pure env flip + redeploy. - skatteverket: redirect_uri pinned via NEXT_PUBLIC_SKV_OAUTH_BASE_URL (Utvecklarportalen registration is slow to change); the OAuth callback now resolves the flow from the state token + stored oauth_user_id via the service client instead of session cookies, which no longer exist on the OAuth host. Legacy same-domain flows fall back to the session. - popup listeners (SkatteverketConnectPanel, AGIPanel) accept postMessage from the pinned OAuth origin; event.source identity check unchanged. - /.well-known discovery docs reflect the allowlisted request host so existing MCP connectors on app.gnubok.se keep a self-consistent issuer/resource after the flip; spoofed hosts fall back to canonical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: log dual-domain cutover decision Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): recency-bound SKV state lookup, exact localhost match in discovery allowlist - The oauth_state lookup now only considers rows updated in the last 10 minutes: bounds how long a leaked/phished authorize URL stays completable, keeps the row set far below PostgREST's 1000-row cap, and surfaces query errors instead of misreporting them as CSRF. - resolveDiscoveryBaseUrl matches localhost/127.0.0.1 exactly; the prefix check reflected spoofed hosts like localhost.evil.example. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3d97b95197
commit
b420f3e1d9
@@ -23,6 +23,7 @@ import { InfoTooltip } from '@/components/ui/info-tooltip'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { UpgradeNote } from '@/components/billing/UpgradeNote'
|
||||
import { useCapability } from '@/contexts/CompanyContext'
|
||||
import { isAllowedSkvPopupOrigin } from '@/lib/skatteverket/popup-origin'
|
||||
import { CAPABILITY } from '@/lib/entitlements/keys'
|
||||
import type { AgiSubmissionState } from '@/lib/salary/agi-submission-state'
|
||||
|
||||
@@ -244,7 +245,9 @@ export function AGIPanel(props: AGIPanelProps) {
|
||||
// "expired" / not-connected to "Ansluten" without a full page reload.
|
||||
useEffect(() => {
|
||||
function handleMessage(event: MessageEvent) {
|
||||
if (event.origin !== window.location.origin) return
|
||||
// The popup runs on the pinned SKV OAuth host, which differs from the
|
||||
// app origin after the app.accounted.se cutover.
|
||||
if (!isAllowedSkvPopupOrigin(event.origin, window.location.origin)) return
|
||||
// Source-identity check: only the popup this component opened can
|
||||
// trigger the handler; a window reference cannot be forged by other
|
||||
// same-origin scripts.
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { useCapability } from '@/contexts/CompanyContext'
|
||||
import { isAllowedSkvPopupOrigin } from '@/lib/skatteverket/popup-origin'
|
||||
import { CAPABILITY } from '@/lib/entitlements/keys'
|
||||
import { UpgradeNote } from '@/components/billing/UpgradeNote'
|
||||
import { CheckCircle2, ExternalLink, ShieldOff, FlaskConical, ShieldAlert } from 'lucide-react'
|
||||
@@ -130,7 +131,9 @@ function SkatteverketPersonalConnectionCard() {
|
||||
// the settings page never navigates and we just re-fetch the status.
|
||||
useEffect(() => {
|
||||
function handleMessage(event: MessageEvent) {
|
||||
if (event.origin !== window.location.origin) return
|
||||
// The popup runs on the pinned SKV OAuth host, which differs from the
|
||||
// app origin after the app.accounted.se cutover.
|
||||
if (!isAllowedSkvPopupOrigin(event.origin, window.location.origin)) return
|
||||
// Source-identity check: only the popup this component opened can
|
||||
// trigger the handler; a window reference cannot be forged by other
|
||||
// same-origin scripts.
|
||||
|
||||
Reference in New Issue
Block a user