M12 / ST-1110: Demo gebruiker read-only (#17)
* feat(ST-1110.3): add proxy.ts demo-guard for non-GET API routes
* feat(ST-1110.3+4): demo-guard proxy + block demo in QR-pairing
- proxy.ts: gebruik unsealData ipv getIronSession (middleware-compatibel)
- pair/start: isDemo-check via cookies() guard
- pair/claim: check pairing.user.is_demo na DB-read; 403 + clearPairCookie
* feat(ST-1110.5): unify demo write-button pattern to disabled+tooltip
Convert all !isDemo && <Button> patterns to <DemoTooltip show={isDemo}>
<Button disabled={isDemo}> so demo visitors see app capabilities.
Affects: pbi-list, story-panel, story-dialog, task-list, sprint-backlog,
token-manager, product-list, activate-product-button, leave-product-button,
settings page.
* test(ST-1110.6): proxy demo-guard coverage — 403 for demo+non-GET on /api/*
* docs(ST-1110.7): document three-layer demo-readonly policy and mirror plan
This commit is contained in:
parent
8a9fb9d32b
commit
1cb5772edd
19 changed files with 413 additions and 142 deletions
|
|
@ -32,6 +32,7 @@ import { reorderPbisAction, updatePbiPriorityAction } from '@/actions/stories'
|
|||
import { cn } from '@/lib/utils'
|
||||
import { PbiDialog, type PbiDialogState } from './pbi-dialog'
|
||||
import { BacklogCard } from './backlog-card'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { PRIORITY_COLORS } from '@/components/shared/priority-select'
|
||||
import { PBI_STATUS_LABELS, PBI_STATUS_COLORS } from '@/components/shared/pbi-status-select'
|
||||
import type { PbiStatusApi } from '@/lib/task-status'
|
||||
|
|
@ -164,24 +165,30 @@ function SortablePbiRow({
|
|||
{PBI_STATUS_LABELS[pbi.status]}
|
||||
</Badge>
|
||||
}
|
||||
actions={!isDemo ? (
|
||||
actions={
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onEdit() }}
|
||||
className="border border-border rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:text-foreground hover:bg-surface-container transition-colors"
|
||||
aria-label="Bewerk PBI"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onDelete() }}
|
||||
className="text-muted-foreground hover:text-error text-xs"
|
||||
aria-label="Verwijder PBI"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); if (!isDemo) onEdit() }}
|
||||
className="border border-border rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:text-foreground hover:bg-surface-container transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="Bewerk PBI"
|
||||
disabled={isDemo}
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
</DemoTooltip>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); if (!isDemo) onDelete() }}
|
||||
className="text-muted-foreground hover:text-error text-xs disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="Verwijder PBI"
|
||||
disabled={isDemo}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
) : undefined}
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -383,15 +390,16 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
|
|||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{!isDemo && (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => setDialogState({ mode: 'create', productId, defaultPriority: 2 })}
|
||||
disabled={isDemo}
|
||||
onClick={() => !isDemo && setDialogState({ mode: 'create', productId, defaultPriority: 2 })}
|
||||
>
|
||||
+ PBI
|
||||
</Button>
|
||||
)}
|
||||
</DemoTooltip>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
@ -400,11 +408,11 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
|
|||
{pbis.length === 0 ? (
|
||||
<div className="p-8 text-center text-muted-foreground text-sm space-y-3">
|
||||
<p>Nog geen PBI's aangemaakt.</p>
|
||||
{!isDemo && (
|
||||
<Button size="sm" variant="outline" onClick={() => setDialogState({ mode: 'create', productId, defaultPriority: 2 })}>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button size="sm" variant="outline" disabled={isDemo} onClick={() => !isDemo && setDialogState({ mode: 'create', productId, defaultPriority: 2 })}>
|
||||
Maak je eerste PBI aan
|
||||
</Button>
|
||||
)}
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<DndContext
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue