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',