UI/settings api mcp (#524)

* feat(voucher): add create voucher and correct entry previews; update commit methods

* feat: add support for pending operations in API key scopes and OAuth client management

- Introduced new API key scopes for reading and approving pending operations.
- Updated the scope groups to include pending operations.
- Added new tools for listing and managing pending operations.
- Implemented OAuth client registration and revocation endpoints.
- Created a UI panel for managing OAuth clients, including registration and revocation.
- Added tests for pending operations tools and OAuth allowlist functionality.
- Implemented a database migration for OAuth client registrations with appropriate policies and constraints.

* feat: Implement OAuth client registration rate limiting and enhance security measures

- Added IP-based rate limiting to the OAuth client registration endpoint to prevent enumeration attacks.
- Introduced a service-role client for allowlist lookups, ensuring trust boundaries are maintained.
- Updated error responses to be uniform across different types of redirect URI validation failures.
- Enhanced tests to reflect changes in OAuth scope handling, ensuring fallback to read-only scopes when no scopes are provided.
- Improved handling of high-risk pending operations, requiring explicit confirmation for approvals.
- Added audit logging for OAuth client revocations and pending operation approvals/rejections to maintain a security audit trail.
- Refactored API key scope management to include default read-only scopes for OAuth-issued keys and added segregation-of-duties checks.

* feat: add recurring invoice scheduling functionality

- Implemented recurring invoice schedules with a new database schema.
- Created API routes for managing recurring invoices (GET and POST).
- Added cron job to automatically generate invoices based on schedules.
- Developed service functions for computing next run dates and executing schedules.
- Added tests for the new functionality, including validation and success cases.
- Introduced error handling for various scenarios in the invoice creation process.

* feat: refine VAT rate validation and enhance recurring invoice handling
This commit is contained in:
Mattsson
2026-05-19 13:48:32 +02:00
committed by GitHub
parent 16164ea14c
commit e211ab31be
22 changed files with 2133 additions and 76 deletions
+94 -56
View File
@@ -18,6 +18,7 @@ import { Checkbox } from '@/components/ui/checkbox'
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
import { useToast } from '@/components/ui/use-toast'
import { Loader2, Plus, Copy, Check, Trash2, Key, ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
import { getBranding } from '@/lib/branding/service'
import type { ApiKeyScope } from '@/lib/auth/api-keys'
@@ -180,6 +181,48 @@ function CopyBlock({ text }: { text: string }) {
)
}
function ScopeCard({
entry,
checked,
onCheckedChange,
}: {
entry: ScopeEntry
checked: boolean
onCheckedChange: (checked: boolean) => void
}) {
const dashIdx = entry.label.indexOf(' — ')
const verb = dashIdx > 0 ? entry.label.slice(0, dashIdx) : entry.label
const description = dashIdx > 0 ? entry.label.slice(dashIdx + 3) : ''
return (
<label
className={cn(
'flex min-h-[68px] cursor-pointer flex-col gap-1 rounded-md border p-2 transition-colors',
checked
? 'border-foreground/30 bg-secondary'
: 'border-border hover:bg-secondary/60'
)}
>
<div className="flex items-center gap-2">
<Checkbox
checked={checked}
onCheckedChange={onCheckedChange}
className="shrink-0"
/>
<span className="flex-1 text-xs font-medium text-foreground">{verb}</span>
<span className="shrink-0 text-[10px] tabular-nums text-muted-foreground">
{entry.tools > 0 ? `${entry.tools} verktyg` : 'REST'}
</span>
</div>
{description && (
<p className="ml-6 line-clamp-2 text-[11px] leading-snug text-muted-foreground">
{description}
</p>
)}
</label>
)
}
export function ApiKeysPanel() {
const { toast } = useToast()
const { dialogProps: revokeDialogProps, confirm: confirmRevoke } = useDestructiveConfirm()
@@ -331,7 +374,7 @@ export function ApiKeysPanel() {
: `${scopeCount} behörigheter`}
</span>
</div>
<div className="flex items-center gap-3 mt-1">
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
<code className="text-xs text-muted-foreground font-mono">
{key.key_prefix}...
</code>
@@ -433,14 +476,14 @@ export function ApiKeysPanel() {
{/* Create key dialog */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent>
<DialogContent className="max-w-[calc(100vw-2rem)] rounded-2xl p-4 sm:max-w-3xl sm:p-6">
<DialogHeader>
<DialogTitle>Skapa API-nyckel</DialogTitle>
<DialogDescription>
Ge nyckeln ett namn så du vet vad den används till.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-6">
<div className="space-y-2">
<Label htmlFor="key-name">Namn</Label>
<Input
@@ -451,63 +494,58 @@ export function ApiKeysPanel() {
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
/>
</div>
<div className="space-y-2">
<Label>Behörigheter</Label>
<p className="text-xs text-muted-foreground">
Välj vad nyckeln ska ha åtkomst till.
</p>
<div className="space-y-3 pt-1">
<div className="space-y-3">
<div className="flex items-baseline justify-between gap-3">
<div className="space-y-1">
<Label>Behörigheter</Label>
<p className="text-xs text-muted-foreground">
Välj vad nyckeln ska ha åtkomst till.
</p>
</div>
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">
{newKeyScopes.size} av {ALL_SCOPES.length} valda
</span>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{SCOPE_GROUPS.map((group) => (
<div key={group.domain} className="space-y-1.5">
<p className="text-sm font-medium">{group.label}</p>
<div className="space-y-1 pl-1">
<div key={group.domain} className="space-y-2">
<h4 className="text-sm font-medium">{group.label}</h4>
<div className="space-y-2 px-2">
{group.read && (
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={newKeyScopes.has(group.read.scope)}
onCheckedChange={(checked) => {
setNewKeyScopes((prev) => {
const next = new Set(prev)
if (checked) {
next.add(group.read!.scope)
} else {
next.delete(group.read!.scope)
// Remove write too — write without read makes no sense
if (group.write) next.delete(group.write.scope)
}
return next
})
}}
/>
<span className="text-xs text-muted-foreground">{group.read.label}</span>
<span className="text-[10px] tabular-nums text-muted-foreground/60">
{group.read.tools > 0 ? `${group.read.tools} verktyg` : 'REST'}
</span>
</label>
<ScopeCard
entry={group.read}
checked={newKeyScopes.has(group.read.scope)}
onCheckedChange={(checked) => {
setNewKeyScopes((prev) => {
const next = new Set(prev)
if (checked) {
next.add(group.read!.scope)
} else {
next.delete(group.read!.scope)
if (group.write) next.delete(group.write.scope)
}
return next
})
}}
/>
)}
{group.write && (
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={newKeyScopes.has(group.write.scope)}
onCheckedChange={(checked) => {
setNewKeyScopes((prev) => {
const next = new Set(prev)
if (checked) {
next.add(group.write!.scope)
// Auto-check read when write is checked (when read exists)
if (group.read) next.add(group.read.scope)
} else {
next.delete(group.write!.scope)
}
return next
})
}}
/>
<span className="text-xs text-muted-foreground">{group.write.label}</span>
<span className="text-[10px] tabular-nums text-muted-foreground/60">
{group.write.tools > 0 ? `${group.write.tools} verktyg` : 'REST'}
</span>
</label>
<ScopeCard
entry={group.write}
checked={newKeyScopes.has(group.write.scope)}
onCheckedChange={(checked) => {
setNewKeyScopes((prev) => {
const next = new Set(prev)
if (checked) {
next.add(group.write!.scope)
if (group.read) next.add(group.read.scope)
} else {
next.delete(group.write!.scope)
}
return next
})
}}
/>
)}
</div>
</div>