From c3021f1ec64d3c6da2fe15cefd7c69e9da023460 Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Sat, 16 May 2026 10:23:37 +0200 Subject: [PATCH] Bug/supplier invoice input (#504) * feat(settings): add information about Skatteverket's scope for transactions * feat(supplier-invoices): replace Input with Controller for description field in NewSupplierInvoicePage feat(bookkeeping): update AccountCombobox styling by removing height class docs(settings): add documentation link for SkatteverketConnectPanel * refactor(sandbox-seed): remove redundant environment check for sandbox seeding * feat(sandbox-seed): implement rate limiting and IP address handling in sandbox seed endpoint * fix(supplier-invoice): add ref to input fields for better form handling --- .../supplier-invoices/new/page.tsx | 28 +++++++++++++-- app/api/sandbox/seed/route.ts | 35 +++++++++++++------ components/bookkeeping/AccountCombobox.tsx | 2 +- .../settings/SkatteverketConnectPanel.tsx | 1 + 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/app/(dashboard)/supplier-invoices/new/page.tsx b/app/(dashboard)/supplier-invoices/new/page.tsx index cd48a6b8..4ad9647e 100644 --- a/app/(dashboard)/supplier-invoices/new/page.tsx +++ b/app/(dashboard)/supplier-invoices/new/page.tsx @@ -1099,7 +1099,19 @@ export default function NewSupplierInvoicePage() { /> - + ( + + )} + />
- + ( + + )} + />
diff --git a/app/api/sandbox/seed/route.ts b/app/api/sandbox/seed/route.ts index 9473454c..cc47eef1 100644 --- a/app/api/sandbox/seed/route.ts +++ b/app/api/sandbox/seed/route.ts @@ -3,31 +3,44 @@ import { createClient } from '@/lib/supabase/server' import { NextResponse } from 'next/server' import { getActiveCompanyId } from '@/lib/company/context' import { createLogger } from '@/lib/logger' +import { checkRateLimit } from '@/lib/auth/rate-limit-http' +import { truncateIp } from '@/lib/api/v1/with-api-v1' + +// Anonymous sign-in is enabled in all environments so visitors can try the +// product; a per-/24 cap on the seed endpoint keeps a single network from +// spinning up arbitrary sandbox companies. Idempotent for legit users, so 5/h +// covers retries; an attacker has to rotate /24s to scale abuse. +const RATE_LIMIT = { maxRequests: 5, windowMs: 60 * 60 * 1000 } /** * POST /api/sandbox/seed * Seeds demo data for an anonymous sandbox user. * Only callable by anonymous users (is_anonymous === true). - * - * Defense-in-depth: also requires SANDBOX_ENABLED=true. The anonymous-user - * check is the primary control; the env guard exists so that if anonymous - * sign-in is ever turned on accidentally in a production environment, this - * destructive seed endpoint stays inert until an operator explicitly opts in. */ -export async function POST() { +export async function POST(request: Request) { // Per-request logger so seed-failure entries are correlatable in the SIEM. // Cannot reuse withRouteContext here — it requires an active company, but // the sandbox seed runs *before* a company exists for the user. const requestId = `req_${crypto.randomUUID()}` const log = createLogger('sandbox:seed', { requestId }) - if (process.env.SANDBOX_ENABLED !== 'true') { - return NextResponse.json( - { error: 'Sandbox is not enabled in this environment', requestId }, - { status: 403 }, - ) + const fwd = request.headers.get('x-forwarded-for') + const rawIp = fwd ? fwd.split(',')[0]?.trim() : request.headers.get('x-real-ip') ?? undefined + // Fall back to a shared 'unknown' bucket when the proxy doesn't surface a + // client IP — keeps the limit enforced under a misconfigured deploy rather + // than failing open. Truncated /24 elsewhere is the normal path. + const ipIdentifier = truncateIp(rawIp || undefined) ?? 'unknown' + if (rawIp && ipIdentifier === 'unknown') { + log.warn('unparseable forwarded-for header on sandbox seed', { headerLength: rawIp.length }) } + const rl = await checkRateLimit({ + prefix: 'sandbox:seed', + identifier: ipIdentifier, + ...RATE_LIMIT, + }) + if (!rl.ok) return rl.response! + const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() diff --git a/components/bookkeeping/AccountCombobox.tsx b/components/bookkeeping/AccountCombobox.tsx index 4eab866e..cc62982a 100644 --- a/components/bookkeeping/AccountCombobox.tsx +++ b/components/bookkeeping/AccountCombobox.tsx @@ -179,7 +179,7 @@ export default function AccountCombobox({ value, accounts, onChange }: AccountCo onBlur={handleBlur} onKeyDown={handleKeyDown} placeholder="Sök konto…" - className="font-mono h-8" + className="font-mono" autoComplete="off" /> diff --git a/components/settings/SkatteverketConnectPanel.tsx b/components/settings/SkatteverketConnectPanel.tsx index 985b29bd..b4e14be9 100644 --- a/components/settings/SkatteverketConnectPanel.tsx +++ b/components/settings/SkatteverketConnectPanel.tsx @@ -21,6 +21,7 @@ type Status = disabled?: boolean } +// docs: https://www7.skatteverket.se/portal-wapi/open/apier-och-oppna-data/utvecklarportalen/v1/getFile/tjanstebeskrivning-skattekonto-hamta-huvudmans-saldo-och-transaktioner-v101 const SCOPE_LABELS: Record = { momsdeklaration: 'Momsdeklaration', inkforetag: 'Företagsinformation',