Add auth secret reveal panel to Hosted Tables integration card

Admin clicks 'Auth secret', re-enters their password, and sees CENTRAL_AUTH_SECRET
with a copy button. Secret auto-clears after 60 seconds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-15 15:46:41 +00:00
parent 279872364c
commit 880cd9793d

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import { CheckCircle, XCircle, RefreshCw, ChevronDown, ChevronUp, Clock, Folder, FolderOpen, ChevronRight } from 'lucide-react' import { CheckCircle, XCircle, RefreshCw, ChevronDown, ChevronUp, Clock, Folder, FolderOpen, ChevronRight, KeyRound, Copy, Check } from 'lucide-react'
import { Sidebar } from '../components/Sidebar' import { Sidebar } from '../components/Sidebar'
import type { User } from '../types' import type { User } from '../types'
@ -123,6 +123,15 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
const [ncBrowseLoading, setNcBrowseLoading] = useState(false) const [ncBrowseLoading, setNcBrowseLoading] = useState(false)
const [ncBrowseError, setNcBrowseError] = useState<string | null>(null) const [ncBrowseError, setNcBrowseError] = useState<string | null>(null)
// Hosted Tables — auth secret reveal
const [secretPanelOpen, setSecretPanelOpen] = useState(false)
const [secretPassword, setSecretPassword] = useState('')
const [revealedSecret, setRevealedSecret] = useState<string | null>(null)
const [revealing, setRevealing] = useState(false)
const [revealError, setRevealError] = useState<string | null>(null)
const [copied, setCopied] = useState(false)
const secretClearTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
function openForm() { function openForm() {
const initial: Record<string, string> = {} const initial: Record<string, string> = {}
for (const [k, v] of Object.entries(integration.config)) initial[k] = v ?? '' for (const [k, v] of Object.entries(integration.config)) initial[k] = v ?? ''
@ -133,6 +142,51 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
setOpen(true) setOpen(true)
} }
function openSecretPanel() {
setSecretPassword('')
setRevealedSecret(null)
setRevealError(null)
setCopied(false)
if (secretClearTimer.current) clearTimeout(secretClearTimer.current)
setSecretPanelOpen(true)
}
function closeSecretPanel() {
setSecretPanelOpen(false)
setRevealedSecret(null)
setSecretPassword('')
if (secretClearTimer.current) clearTimeout(secretClearTimer.current)
}
async function revealSecret() {
if (!secretPassword) return
setRevealing(true)
setRevealError(null)
const res = await fetch('/api/auth/admin/reveal-secret', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: secretPassword }),
})
const data = await res.json()
setRevealing(false)
if (!res.ok) { setRevealError(data.error || 'Failed'); return }
setRevealedSecret(data.secret)
setSecretPassword('')
// Auto-clear after 60 seconds
if (secretClearTimer.current) clearTimeout(secretClearTimer.current)
secretClearTimer.current = setTimeout(() => {
setRevealedSecret(null)
setCopied(false)
}, 60_000)
}
function copySecret() {
if (!revealedSecret) return
navigator.clipboard.writeText(revealedSecret)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
async function ncBrowse(path: string) { async function ncBrowse(path: string) {
setNcBrowseLoading(true) setNcBrowseLoading(true)
setNcBrowseError(null) setNcBrowseError(null)
@ -206,6 +260,18 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
</div> </div>
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
{integration.slug === 'hosted_tables' && (
<button onClick={secretPanelOpen ? closeSecretPanel : openSecretPanel} style={{
background: 'var(--body-bg)', border: '1px solid var(--card-border)',
borderRadius: '6px', padding: '0.35rem 0.85rem', fontSize: '0.8rem',
fontWeight: 600, color: 'var(--text-dark)', cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: '0.4rem',
}}>
<KeyRound size={13} strokeWidth={1.75} />
{secretPanelOpen ? 'Cancel' : 'Auth secret'}
</button>
)}
<button onClick={open ? () => setOpen(false) : openForm} style={{ <button onClick={open ? () => setOpen(false) : openForm} style={{
background: 'var(--body-bg)', border: '1px solid var(--card-border)', background: 'var(--body-bg)', border: '1px solid var(--card-border)',
borderRadius: '6px', padding: '0.35rem 0.85rem', fontSize: '0.8rem', borderRadius: '6px', padding: '0.35rem 0.85rem', fontSize: '0.8rem',
@ -214,6 +280,75 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
{open ? 'Cancel' : 'Configure'} {open ? 'Cancel' : 'Configure'}
</button> </button>
</div> </div>
</div>
{secretPanelOpen && (
<div style={{ borderTop: '1px solid var(--card-border)', padding: '1.25rem', background: 'var(--body-bg)' }}>
<p style={{ fontSize: '0.8rem', color: 'var(--text-mid)', marginBottom: '1rem' }}>
Re-enter your admin password to reveal the <code style={{ fontSize: '0.78rem', background: 'var(--surface-2)', padding: '0.1em 0.35em', borderRadius: 3 }}>CENTRAL_AUTH_SECRET</code>.
Set this as the <code style={{ fontSize: '0.78rem', background: 'var(--surface-2)', padding: '0.1em 0.35em', borderRadius: 3 }}>CENTRAL_AUTH_SECRET</code> environment variable in Hosted Tables to enable shared session SSO.
</p>
{!revealedSecret ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.3rem' }}>
<span style={{ fontSize: '0.78rem', fontWeight: 600, color: 'var(--text-mid)' }}>Admin password</span>
<input
type="password"
value={secretPassword}
onChange={e => setSecretPassword(e.target.value)}
onKeyDown={e => e.key === 'Enter' && revealSecret()}
placeholder="Your password"
autoFocus
style={inputStyle}
/>
</label>
{revealError && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', fontSize: '0.82rem', color: '#dc2626' }}>
<XCircle size={14} strokeWidth={1.75} /> {revealError}
</div>
)}
<div>
<button onClick={revealSecret} disabled={revealing || !secretPassword} style={{
background: 'var(--navy)', color: '#fff', border: 'none',
borderRadius: '6px', padding: '0.45rem 1.1rem', fontSize: '0.82rem', fontWeight: 600,
cursor: (revealing || !secretPassword) ? 'not-allowed' : 'pointer', opacity: !secretPassword ? 0.5 : 1,
}}>
{revealing ? 'Verifying…' : 'Reveal secret'}
</button>
</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div style={{
display: 'flex', alignItems: 'center', gap: '0.5rem',
background: 'var(--card-bg)', border: '1px solid var(--card-border)',
borderRadius: '6px', padding: '0.55rem 0.75rem',
}}>
<code style={{
flex: 1, fontSize: '0.8rem', color: 'var(--text-dark)',
wordBreak: 'break-all', fontFamily: 'monospace', letterSpacing: '0.02em',
}}>
{revealedSecret}
</code>
<button onClick={copySecret} style={{
background: copied ? '#16a34a' : 'var(--body-bg)',
border: '1px solid var(--card-border)',
borderRadius: '5px', padding: '0.3rem 0.6rem', fontSize: '0.75rem', fontWeight: 600,
cursor: 'pointer', color: copied ? '#fff' : 'var(--text-dark)',
display: 'flex', alignItems: 'center', gap: '0.3rem', flexShrink: 0,
transition: 'all 0.15s',
}}>
{copied ? <><Check size={12} strokeWidth={2} /> Copied</> : <><Copy size={12} strokeWidth={1.75} /> Copy</>}
</button>
</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-mid)' }}>
Secret will be hidden automatically after 60 seconds.
</p>
</div>
)}
</div>
)}
{open && ( {open && (
<div style={{ borderTop: '1px solid var(--card-border)', padding: '1.25rem' }}> <div style={{ borderTop: '1px solid var(--card-border)', padding: '1.25rem' }}>