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:
@@ -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">
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user