mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-07-15 19:03:38 +00:00
feat: robust login with rate limiting + diff propagation across tree
Login hardening: - API: in-memory rate limiter (10 attempts / IP / 15 min), timing-safe credential comparison, strict body validation - Middleware: validate session token timestamp expiry (7-day window) not just HMAC signature - Logout: also clears oidc_token_pub cookie - Login page: try/catch around fetch to surface network errors, parse OIDC error query params, disable inputs during loading, wrap useSearchParams in Suspense Diff propagation: - DiffViewer: shows inherited ancestor patches in a collapsible section above own patches - CVTree: annotates each branch with ↑N ancestor patch count badge; propagate (↓) button on hover for versions with own patches and child branches - Dashboard: computeInheritedPatches/getDescendants helpers; Propagate ↓ action button in version header; handlePropagate sends current version's patches to all descendants via appendPatches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Stdyi4YmG1BUSurArZPe8w
This commit is contained in:
@@ -1,13 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Patch } from '@/libs/api';
|
||||
|
||||
const OP_SYMBOL: Record<string, string> = {
|
||||
replace_text: '±', remove_block: '−', reorder_section: '↕', boost_keyword: '+',
|
||||
};
|
||||
|
||||
export default function DiffViewer({ patches }: { patches: Patch[] }) {
|
||||
if (!patches.length) {
|
||||
function PatchRow({ p, dim }: { p: Patch; dim?: boolean }) {
|
||||
return (
|
||||
<div style={{ borderLeft: '2px solid var(--border-strong)', paddingLeft: 12, opacity: dim ? 0.6 : 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)' }}>
|
||||
{OP_SYMBOL[p.operation] ?? '·'} {p.target_path}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-faint)' }}>{p.operation}</span>
|
||||
</div>
|
||||
{p.old_value && (
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.5, padding: '3px 6px', background: '#fef2f2', borderRadius: 3, marginBottom: 3, color: '#991b1b', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
− {p.old_value}
|
||||
</div>
|
||||
)}
|
||||
{p.new_value && (
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.5, padding: '3px 6px', background: '#f0fdf4', borderRadius: 3, color: '#166534', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
+ {p.new_value}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DiffViewer({
|
||||
patches, inheritedPatches, ancestorLabel,
|
||||
}: {
|
||||
patches: Patch[];
|
||||
inheritedPatches?: Patch[];
|
||||
ancestorLabel?: string;
|
||||
}) {
|
||||
const [showInherited, setShowInherited] = useState(false);
|
||||
const hasInherited = (inheritedPatches?.length ?? 0) > 0;
|
||||
|
||||
if (!patches.length && !hasInherited) {
|
||||
return (
|
||||
<div style={{ padding: '20px 0', color: 'var(--text-faint)', fontSize: 13 }}>
|
||||
No patches — identical to parent.
|
||||
@@ -16,27 +49,37 @@ export default function DiffViewer({ patches }: { patches: Patch[] }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{patches.map(p => (
|
||||
<div key={p.id} style={{ borderLeft: '2px solid var(--border-strong)', paddingLeft: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)' }}>
|
||||
{OP_SYMBOL[p.operation] ?? '·'} {p.target_path}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-faint)' }}>{p.operation}</span>
|
||||
</div>
|
||||
{p.old_value && (
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.5, padding: '3px 6px', background: '#fef2f2', borderRadius: 3, marginBottom: 3, color: '#991b1b', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
− {p.old_value}
|
||||
</div>
|
||||
)}
|
||||
{p.new_value && (
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.5, padding: '3px 6px', background: '#f0fdf4', borderRadius: 3, color: '#166534', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
+ {p.new_value}
|
||||
</div>
|
||||
<div>
|
||||
{hasInherited && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
style={{ fontSize: 11, padding: '2px 8px' }}
|
||||
onClick={() => setShowInherited(v => !v)}
|
||||
>
|
||||
{showInherited ? '▾' : '▸'} Inherited from ancestors ({inheritedPatches!.length})
|
||||
</button>
|
||||
{ancestorLabel && (
|
||||
<span style={{ fontSize: 11, color: 'var(--text-faint)' }}>via {ancestorLabel}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
|
||||
{showInherited && hasInherited && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 18, paddingBottom: 14, borderBottom: '1px dashed var(--border)' }}>
|
||||
{inheritedPatches!.map(p => <PatchRow key={p.id} p={p} dim />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{patches.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{patches.map(p => <PatchRow key={p.id} p={p} />)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: '8px 0', color: 'var(--text-faint)', fontSize: 13 }}>
|
||||
No direct patches on this branch.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user