204 lines
8.2 KiB
TypeScript
204 lines
8.2 KiB
TypeScript
import { createOperationKey } from '@/lib/operation-key';
|
|
import { useState } from 'react';
|
|
import { Management, button, control } from '@/pages/management/shared';
|
|
import { profileJson } from '@/lib/profile-json';
|
|
|
|
type Plan = {
|
|
installed: boolean;
|
|
consistent: boolean;
|
|
ready: boolean;
|
|
target: { database: string; port: number };
|
|
hash: string;
|
|
dependencies: Record<string, boolean>;
|
|
changes: string[];
|
|
backupDate: string | null;
|
|
rollback: string;
|
|
bootstrap: {
|
|
initialized: boolean;
|
|
holders: number[];
|
|
version: number | null;
|
|
rule: string;
|
|
pending: {approval_id:string;confirmation_url:string;expires_at:string} | null;
|
|
};
|
|
};
|
|
export default function Installation(initial: Plan) {
|
|
const [plan, setPlan] = useState(initial);
|
|
const [reason, setReason] = useState('');
|
|
const [confirmed, setConfirmed] = useState(false);
|
|
const [first, setFirst] = useState('');
|
|
const [second, setSecond] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
const [message, setMessage] = useState('');
|
|
const [confirmationUrl, setConfirmationUrl] = useState<string | null>(null);
|
|
const [installationKey] = useState(createOperationKey);
|
|
const [bootstrapKey] = useState(createOperationKey);
|
|
async function run(bootstrap = false) {
|
|
setBusy(true);
|
|
setMessage('');
|
|
try {
|
|
const result = await profileJson(
|
|
'/management/profile-install' + (bootstrap ? '/bootstrap' : ''),
|
|
{
|
|
hash: plan.hash,
|
|
confirm: confirmed,
|
|
reason,
|
|
idempotency_key: bootstrap ? bootstrapKey : installationKey,
|
|
...(bootstrap
|
|
? {
|
|
first_user_id: Number(first),
|
|
second_user_id: Number(second),
|
|
}
|
|
: {}),
|
|
},
|
|
);
|
|
setMessage(
|
|
bootstrap
|
|
? 'Pedido registado, sem atribuir privilégios. O segundo titular deve autenticar-se com MFA e confirmar pelo endereço abaixo.'
|
|
: result.changed
|
|
? 'Schema instalado. Perfis ainda não disponibilizados; nenhuma conta ou atribuição criada.'
|
|
: 'Schema já instalado. Nenhuma reinstalação ou duplicação.',
|
|
);
|
|
if (bootstrap) setConfirmationUrl(result.confirmation_url as string);
|
|
if (!bootstrap)
|
|
setPlan(
|
|
(await profileJson(
|
|
'/management/profile-install',
|
|
{},
|
|
'GET',
|
|
)) as Plan,
|
|
);
|
|
setConfirmed(false);
|
|
} catch (error) {
|
|
setMessage(
|
|
error instanceof Error
|
|
? error.message
|
|
: 'Resposta indisponível; consulte o estado antes de repetir.',
|
|
);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
return (
|
|
<Management title="Instalação dos perfis — 040000">
|
|
<p>
|
|
Base {plan.target.database}:{plan.target.port} ·{' '}
|
|
{plan.installed ? 'Schema instalado' : 'Schema por instalar'}
|
|
</p>
|
|
<p>
|
|
Preparação:{' '}
|
|
{plan.ready
|
|
? 'Provas atuais verificadas'
|
|
: 'Dependências ou provas por completar'}
|
|
. Preparação não significa instalação ou disponibilização dos
|
|
perfis.
|
|
</p>
|
|
{!plan.consistent && (
|
|
<p role="alert">
|
|
Ledger e schema divergem. Recuperar a consistência antes de
|
|
instalar.
|
|
</p>
|
|
)}
|
|
<ul>
|
|
{Object.entries(plan.dependencies).map(([key, ok]) => (
|
|
<li key={key}>
|
|
{key}: {ok ? 'presente' : 'pendente'}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<ul>
|
|
{plan.changes.map((c) => (
|
|
<li key={c}>{c}</li>
|
|
))}
|
|
</ul>
|
|
<p>Backup/restauro: {plan.backupDate ?? 'Prova atual pendente'}</p>
|
|
<p>{plan.rollback}</p>
|
|
<label className="block">
|
|
Fundamento
|
|
<textarea
|
|
className={control}
|
|
value={reason}
|
|
minLength={20}
|
|
maxLength={1000}
|
|
onChange={(e) => setReason(e.target.value)}
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
<input
|
|
type="checkbox"
|
|
checked={confirmed}
|
|
onChange={(e) => setConfirmed(e.target.checked)}
|
|
/>{' '}
|
|
Confirmo exclusivamente a ação indicada nesta base
|
|
</label>
|
|
<button
|
|
className={button}
|
|
disabled={
|
|
busy ||
|
|
!confirmed ||
|
|
!plan.ready ||
|
|
plan.installed ||
|
|
!plan.consistent ||
|
|
reason.length < 20
|
|
}
|
|
onClick={() => void run()}
|
|
>
|
|
Instalar schema 040000
|
|
</button>
|
|
<section className="space-y-3 rounded border p-4">
|
|
<h2>Inicialização separada dos dois superutilizadores</h2>
|
|
<p>{plan.bootstrap.rule}</p>
|
|
{plan.bootstrap.pending && <p>Pedido pendente até {plan.bootstrap.pending.expires_at}. <a className="underline" href={plan.bootstrap.pending.confirmation_url}>Recuperar pedido de confirmação</a>. Nenhum privilégio foi atribuído pelo pedido.</p>}
|
|
{plan.bootstrap.initialized ? (
|
|
<p>
|
|
Dupla já inicializada:{' '}
|
|
{plan.bootstrap.holders.join(' e ')}; versão{' '}
|
|
{plan.bootstrap.version}. Use a substituição atómica
|
|
autorizada; não reinicialize.
|
|
</p>
|
|
) : (
|
|
<>
|
|
<label className="block">
|
|
ID da primeira conta explicitamente designada
|
|
<input
|
|
className={control}
|
|
type="number"
|
|
min={1}
|
|
value={first}
|
|
onChange={(e) => setFirst(e.target.value)}
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
ID da segunda conta explicitamente designada
|
|
<input
|
|
className={control}
|
|
type="number"
|
|
min={1}
|
|
value={second}
|
|
onChange={(e) => setSecond(e.target.value)}
|
|
/>
|
|
</label>
|
|
<button
|
|
className={button}
|
|
disabled={
|
|
busy ||
|
|
!confirmed ||
|
|
!plan.installed ||
|
|
!!plan.bootstrap.pending ||
|
|
!first ||
|
|
!second ||
|
|
first === second ||
|
|
reason.length < 20
|
|
}
|
|
onClick={() => void run(true)}
|
|
>
|
|
Pedir confirmação ao segundo titular
|
|
</button>
|
|
</>
|
|
)}
|
|
</section>
|
|
<p role="status">{message}</p>
|
|
{confirmationUrl && <p>Endereço para o segundo titular: <a className="underline" href={confirmationUrl}>{confirmationUrl}</a>. O primeiro titular pode consultar, mas não confirmar.</p>}
|
|
</Management>
|
|
);
|
|
}
|