diff --git a/app/(app)/customize/panels/permissions-panel.tsx b/app/(app)/customize/panels/permissions-panel.tsx index 362431b..3cec5b4 100644 --- a/app/(app)/customize/panels/permissions-panel.tsx +++ b/app/(app)/customize/panels/permissions-panel.tsx @@ -60,6 +60,14 @@ export function PermissionsPanel({ isAdmin, customRoles, rolePerms }: Props) { holds all permissions (implicit, locked) + {!isAdmin && ( +
+

+ View only. Only an admin can change permissions. +

+
+ )} +
@@ -118,6 +126,7 @@ function PermGroup({ onToggle(s.key, p.code, e.target.checked)} /> diff --git a/app/(app)/ops/ops-client.tsx b/app/(app)/ops/ops-client.tsx index 6254b16..9161a76 100644 --- a/app/(app)/ops/ops-client.tsx +++ b/app/(app)/ops/ops-client.tsx @@ -1,8 +1,8 @@ 'use client' -import { useState, useMemo } from 'react' +import { useState, useMemo, useEffect, useRef } from 'react' import { useRouter } from 'next/navigation' -import { Plus, Search, Lock, Pin, Eye, EyeOff, Pencil, Trash2, ChevronDown, Users2, FileText } from 'lucide-react' +import { Plus, Search, Lock, Pin, Eye, EyeOff, Pencil, Trash2, ChevronDown, Users2, FileText, Copy, Check } from 'lucide-react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { createKbEntry, updateKbEntry, deleteKbEntry } from '@/lib/actions' @@ -359,17 +359,50 @@ function SecretRow({ secret, onDelete, canManageAcl, aclRoleOptions, aclInitial const [value, setValue] = useState(null) const [loading, setLoading] = useState(false) const [showAcl, setShowAcl] = useState(false) + const [copied, setCopied] = useState(false) + const hideTimer = useRef | null>(null) + + function hide() { + if (hideTimer.current) { clearTimeout(hideTimer.current); hideTimer.current = null } + setRevealed(false) + setValue(null) + setCopied(false) + } async function reveal() { - if (revealed) { setRevealed(false); setValue(null); return } + if (revealed) { hide(); return } setLoading(true) const result = await revealSecret(secret.id) setLoading(false) if (result.error) { toast.error(result.error); return } setValue(result.value ?? '') setRevealed(true) + if (hideTimer.current) clearTimeout(hideTimer.current) + hideTimer.current = setTimeout(hide, 30000) } + async function copy() { + if (!value) return + try { + await navigator.clipboard.writeText(value) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } catch { + toast.error('Could not copy to clipboard') + } + } + + // Re-hide a revealed secret when the window loses focus (screen share / + // shoulder surfing), and drop the plaintext + timer on unmount. + useEffect(() => { + const onBlur = () => hide() + window.addEventListener('blur', onBlur) + return () => { + window.removeEventListener('blur', onBlur) + if (hideTimer.current) clearTimeout(hideTimer.current) + } + }, []) + async function handleDelete() { if (!(await confirm({ title: 'Delete secret', description: `"${secret.title}" cannot be undone.`, confirmText: 'Delete', destructive: true }))) return const result = await deleteSecret(secret.id) @@ -388,7 +421,18 @@ function SecretRow({ secret, onDelete, canManageAcl, aclRoleOptions, aclInitial

{secret.title}

{secret.area &&

{secret.area}

} {revealed && value && ( -

{value}

+
+

{value}

+ +
)}
diff --git a/components/ops/ops-acl-editor.tsx b/components/ops/ops-acl-editor.tsx index d8f6824..0e94521 100644 --- a/components/ops/ops-acl-editor.tsx +++ b/components/ops/ops-acl-editor.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { toast } from 'sonner' +import { Check } from 'lucide-react' import { setOpsAcl } from '@/lib/actions' export interface AclRoleOption { value: string; label: string } @@ -37,6 +38,8 @@ export function OpsAclEditor({ entityType, entityId, options, initial }: Props) else back.add(value) return back }) + } else { + toast.success('Access updated') } } @@ -55,11 +58,13 @@ export function OpsAclEditor({ entityType, entityId, options, initial }: Props) )