256 lines
9.7 KiB
TypeScript
256 lines
9.7 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';
|
|
import { Link } from '@inertiajs/react';
|
|
import { usePagePreparationGuard } from '@/lib/page-preparation-guard';
|
|
|
|
type Plan = {
|
|
technicalSupportUpdateUrl?: string;
|
|
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);
|
|
const [confirmedPreparation, setConfirmedPreparation] = useState({
|
|
reason: '',
|
|
first: '',
|
|
second: '',
|
|
});
|
|
usePagePreparationGuard(
|
|
busy ||
|
|
reason !== confirmedPreparation.reason ||
|
|
first !== confirmedPreparation.first ||
|
|
second !== confirmedPreparation.second,
|
|
'Existe uma preparação ou uma operação de perfis em curso. Sair desta página?',
|
|
);
|
|
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),
|
|
}
|
|
: {}),
|
|
},
|
|
);
|
|
setConfirmedPreparation((previous) => ({
|
|
...previous,
|
|
reason,
|
|
...(bootstrap ? { first, 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">
|
|
{plan.technicalSupportUpdateUrl && (
|
|
<Link
|
|
className="underline"
|
|
href={plan.technicalSupportUpdateUrl}
|
|
>
|
|
Actualização do Suporte técnico
|
|
</Link>
|
|
)}
|
|
<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>
|
|
);
|
|
}
|