Register hosts under Containers/VMs/Docker with encrypted key storage, and require re-authentication for sensitive account changes.
97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import { useState, type FormEvent } from 'react'
|
|
import type { ReauthCredentials } from './api/client'
|
|
|
|
type ReauthModalProps = {
|
|
title: string
|
|
hint?: string
|
|
requireTotp: boolean
|
|
confirmLabel?: string
|
|
onCancel: () => void
|
|
onConfirm: (credentials: ReauthCredentials) => Promise<void>
|
|
}
|
|
|
|
export function ReauthModal({
|
|
title,
|
|
hint,
|
|
requireTotp,
|
|
confirmLabel = 'Confirm',
|
|
onCancel,
|
|
onConfirm,
|
|
}: ReauthModalProps) {
|
|
const [password, setPassword] = useState('')
|
|
const [totpCode, setTotpCode] = useState('')
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
|
|
async function handleSubmit(event: FormEvent) {
|
|
event.preventDefault()
|
|
setError(null)
|
|
setIsSubmitting(true)
|
|
try {
|
|
await onConfirm({
|
|
current_password: password,
|
|
totp_code: totpCode,
|
|
})
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Re-authentication failed')
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="reauth-backdrop" role="presentation">
|
|
<div
|
|
className="reauth-modal"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="reauth-title"
|
|
>
|
|
<h2 id="reauth-title">{title}</h2>
|
|
{hint ? <p className="config-hint">{hint}</p> : null}
|
|
<form className="reauth-form" onSubmit={(event) => void handleSubmit(event)}>
|
|
<label htmlFor="reauth-password">Password</label>
|
|
<input
|
|
id="reauth-password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
value={password}
|
|
onChange={(event) => setPassword(event.target.value)}
|
|
required
|
|
/>
|
|
{requireTotp ? (
|
|
<>
|
|
<label htmlFor="reauth-totp">Authenticator code</label>
|
|
<input
|
|
id="reauth-totp"
|
|
type="text"
|
|
inputMode="numeric"
|
|
autoComplete="one-time-code"
|
|
value={totpCode}
|
|
onChange={(event) => setTotpCode(event.target.value)}
|
|
required
|
|
/>
|
|
</>
|
|
) : null}
|
|
{error ? (
|
|
<p className="groups-error" role="alert">
|
|
{error}
|
|
</p>
|
|
) : null}
|
|
<div className="reauth-actions">
|
|
<button type="button" className="config-action-revert" onClick={onCancel}>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="config-action-save"
|
|
disabled={isSubmitting || !password}
|
|
>
|
|
{isSubmitting ? 'Checking…' : confirmLabel}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|