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:
parent
279872364c
commit
880cd9793d
1 changed files with 144 additions and 9 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { CheckCircle, XCircle, RefreshCw, ChevronDown, ChevronUp, Clock, Folder, FolderOpen, ChevronRight } from 'lucide-react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { CheckCircle, XCircle, RefreshCw, ChevronDown, ChevronUp, Clock, Folder, FolderOpen, ChevronRight, KeyRound, Copy, Check } from 'lucide-react'
|
||||
import { Sidebar } from '../components/Sidebar'
|
||||
import type { User } from '../types'
|
||||
|
||||
|
|
@ -123,6 +123,15 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
|
|||
const [ncBrowseLoading, setNcBrowseLoading] = useState(false)
|
||||
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() {
|
||||
const initial: Record<string, string> = {}
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
setNcBrowseLoading(true)
|
||||
setNcBrowseError(null)
|
||||
|
|
@ -206,6 +260,18 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
|
|||
</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={{
|
||||
background: 'var(--body-bg)', border: '1px solid var(--card-border)',
|
||||
borderRadius: '6px', padding: '0.35rem 0.85rem', fontSize: '0.8rem',
|
||||
|
|
@ -214,6 +280,75 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
|
|||
{open ? 'Cancel' : 'Configure'}
|
||||
</button>
|
||||
</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 && (
|
||||
<div style={{ borderTop: '1px solid var(--card-border)', padding: '1.25rem' }}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue