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
This commit is contained in:
Mattsson
2026-05-16 10:23:37 +02:00
committed by GitHub
parent 9b6ac0f4a6
commit c3021f1ec6
4 changed files with 52 additions and 14 deletions
+26 -2
View File
@@ -1099,7 +1099,19 @@ export default function NewSupplierInvoicePage() {
/>
</td>
<td className="py-2 pr-2">
<Input placeholder="Beskrivning" {...register(`items.${index}.description`)} />
<Controller
name={`items.${index}.description`}
control={control}
render={({ field }) => (
<Input
placeholder="Beskrivning"
ref={field.ref}
value={field.value ?? ''}
onChange={field.onChange}
onBlur={field.onBlur}
/>
)}
/>
</td>
<td className="py-2 pr-2">
<Controller
@@ -1177,7 +1189,19 @@ export default function NewSupplierInvoicePage() {
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Beskrivning</Label>
<Input placeholder="Beskrivning" {...register(`items.${index}.description`)} />
<Controller
name={`items.${index}.description`}
control={control}
render={({ field }) => (
<Input
placeholder="Beskrivning"
ref={field.ref}
value={field.value ?? ''}
onChange={field.onChange}
onBlur={field.onBlur}
/>
)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
+24 -11
View File
@@ -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()
+1 -1
View File
@@ -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"
/>
@@ -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<string, string> = {
momsdeklaration: 'Momsdeklaration',
inkforetag: 'Företagsinformation',