Files
Investidor-V2/resources/js/lib/profile-support-installation.ts
T

407 lines
14 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { portalRequest, PortalApiError } from './portal-api';
import { createOperationKey } from './operation-key';
import { usePagePreparationGuard } from './page-preparation-guard';
export type SupportPlan = {
actorId: number;
target: { database: string; port: number };
hash: string;
code_hash: string;
installed: boolean;
consistent: boolean;
repairable: boolean;
ready: boolean;
action: 'none' | 'install' | 'repair_known_base' | 'blocked';
schema: { state: string; ledger: boolean };
dependencies: Record<string, boolean>;
changes: string[];
backupDate: string | null;
rollback: string;
commands: { key: string; code_hash: string }[];
mfa_confirmed: boolean;
};
type Attempt = {
body: {
hash: string;
reason: string;
confirm: true;
idempotency_key: string;
};
code: string;
generation: number;
};
const endpoint = '/management/profile-install/technical-support';
const object = (v: unknown): v is Record<string, unknown> =>
!!v && typeof v === 'object' && !Array.isArray(v);
const hex = (v: unknown): v is string =>
typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
const target = (v: unknown): v is SupportPlan['target'] =>
object(v) &&
typeof v.database === 'string' &&
Number.isSafeInteger(v.port) &&
Number(v.port) > 0;
const validPlan = (v: unknown): v is SupportPlan =>
object(v) &&
typeof v.actorId === 'number' &&
Number.isSafeInteger(v.actorId) &&
v.actorId > 0 &&
target(v.target) &&
hex(v.hash) &&
hex(v.code_hash) &&
typeof v.installed === 'boolean' &&
typeof v.consistent === 'boolean' &&
typeof v.repairable === 'boolean' &&
typeof v.ready === 'boolean' &&
['none', 'install', 'repair_known_base', 'blocked'].includes(
String(v.action),
) &&
object(v.schema) &&
typeof v.schema.state === 'string' &&
typeof v.schema.ledger === 'boolean' &&
object(v.dependencies) &&
Object.values(v.dependencies).every((x) => typeof x === 'boolean') &&
Array.isArray(v.changes) &&
v.changes.every((x) => typeof x === 'string') &&
(v.backupDate === null || typeof v.backupDate === 'string') &&
typeof v.rollback === 'string' &&
typeof v.mfa_confirmed === 'boolean' &&
Array.isArray(v.commands) &&
v.commands.every(
(x) => object(x) && typeof x.key === 'string' && hex(x.code_hash),
);
const sameTarget = (a: SupportPlan['target'], b: SupportPlan['target']) =>
a.database === b.database && a.port === b.port;
export function useProfileSupportInstallation(initial: SupportPlan) {
const [plan, setPlan] = useState(initial),
[base, setBase] = useState(initial),
[reason, setReason] = useState(''),
[confirmed, setConfirmed] = useState(false),
[busy, setBusy] = useState(false),
[refreshing, setRefreshing] = useState(false),
[uncertain, setUncertain] = useState(false),
[blocked, setBlocked] = useState(false),
[needsRefresh, setNeedsRefresh] = useState(false),
[message, setMessage] = useState(''),
[error, setError] = useState('');
const origin = useRef({ actor: initial.actorId, target: initial.target }),
epoch = useRef({ actor: initial.actorId, target: initial.target }),
alive = useRef(true),
pending = useRef<Attempt | null>(null),
active = useRef<Attempt | null>(null),
reading = useRef<object | null>(null),
generation = useRef(0),
text = useRef(reason),
committed = useRef<Attempt | null>(null);
if (
epoch.current.actor !== initial.actorId ||
!sameTarget(epoch.current.target, initial.target)
)
epoch.current = { actor: initial.actorId, target: initial.target };
text.current = reason;
const valid =
validPlan(plan) &&
plan.actorId === origin.current.actor &&
sameTarget(plan.target, origin.current.target) &&
initial.actorId === origin.current.actor &&
sameTarget(initial.target, origin.current.target);
const stale = base.hash !== plan.hash;
useEffect(() => {
alive.current = true;
return () => {
alive.current = false;
};
}, []);
function change(value: string) {
generation.current++;
text.current = value;
setReason(value);
setConfirmed(false);
setError('');
}
function acknowledge(attempt: Attempt) {
if (pending.current !== attempt) return;
pending.current = null;
active.current = null;
committed.current = attempt;
setBusy(false);
setUncertain(false);
setNeedsRefresh(true);
setConfirmed(false);
setError('');
setMessage(
'Resultado da actualização confirmado. Confira a prévia actual.',
);
if (generation.current === attempt.generation) {
text.current = '';
setReason('');
}
}
function read(value: unknown) {
if (
!validPlan(value) ||
value.actorId !== origin.current.actor ||
!sameTarget(value.target, origin.current.target)
)
return false;
setPlan(value);
setBlocked(false);
setError('');
const attempt = pending.current;
if (
attempt &&
value.commands.some(
(c) =>
c.key === attempt.body.idempotency_key &&
c.code_hash === attempt.code,
)
)
acknowledge(attempt);
const known = committed.current;
if (known) {
if (
!value.commands.some(
(c) =>
c.key === known.body.idempotency_key &&
c.code_hash === known.code,
)
) {
setError(
'Esta consulta ainda não confirma o resultado já recebido. Actualize novamente antes de outra acção.',
);
return true;
}
if (
value.code_hash === known.code &&
value.installed &&
value.consistent
)
setBase(value);
committed.current = null;
setNeedsRefresh(false);
} else setNeedsRefresh(false);
return true;
}
useEffect(() => {
if (validPlan(initial)) read(initial);
}, [initial]);
async function refresh() {
if (reading.current) return;
const request = {},
scope = epoch.current;
reading.current = request;
setRefreshing(true);
try {
const value = await portalRequest<unknown>(endpoint);
if (alive.current && epoch.current === scope && !read(value))
setError(
'A consulta recebida não corresponde a esta conta e destino. A preparação foi conservada.',
);
} catch (e) {
if (alive.current && epoch.current === scope) {
if (
e instanceof PortalApiError &&
[401, 403, 419].includes(e.status)
)
setBlocked(true);
setError(
'Não foi possível actualizar a prévia. O fundamento e a tentativa original foram conservados.',
);
}
} finally {
if (
alive.current &&
epoch.current === scope &&
reading.current === request
) {
reading.current = null;
setRefreshing(false);
}
}
}
async function send(retry = false) {
if (
!valid ||
blocked ||
!plan.mfa_confirmed ||
active.current ||
reading.current
)
return;
if (
!retry &&
(pending.current ||
stale ||
needsRefresh ||
!confirmed ||
!plan.ready ||
plan.installed ||
!plan.mfa_confirmed ||
(!plan.consistent && !plan.repairable))
)
return;
let attempt = pending.current;
if (!attempt) {
if (text.current.trim().length < 20 || text.current.length > 1000) {
setError('Preencha o fundamento com 20 a 1000 caracteres.');
return;
}
try {
attempt = {
body: {
hash: base.hash,
reason: text.current,
confirm: true,
idempotency_key: createOperationKey().toLowerCase(),
},
code: base.code_hash,
generation: generation.current,
};
} catch (e) {
setError(
e instanceof Error
? e.message
: 'Não foi possível preparar a operação.',
);
return;
}
pending.current = attempt;
}
const scope = epoch.current;
active.current = attempt;
setBusy(true);
setUncertain(true);
setError('');
try {
const result = await portalRequest<unknown>(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(attempt.body),
});
if (
!alive.current ||
epoch.current !== scope ||
pending.current !== attempt
)
return;
if (
!object(result) ||
result.actor_id !== origin.current.actor ||
result.command_key !== attempt.body.idempotency_key ||
result.migration !==
'2026_11_03_080000_add_technical_support_profile' ||
result.code_hash !== attempt.code ||
result.installed !== true ||
!target(result.target) ||
!sameTarget(result.target, origin.current.target)
)
throw new Error(
'A confirmação não corresponde à tentativa preparada. Consulte a prévia antes de continuar.',
);
acknowledge(attempt);
void refresh();
} catch (e) {
if (
!alive.current ||
epoch.current !== scope ||
pending.current !== attempt
)
return;
if (e instanceof PortalApiError && [409, 422].includes(e.status)) {
pending.current = null;
setUncertain(false);
setConfirmed(false);
setError(
e.status === 409
? 'A tentativa foi recusada porque o plano mudou. Actualize e compare a prévia; o fundamento foi conservado.'
: e.errors.reason?.join(' ') ||
'A operação foi recusada. Confira as dependências, as provas e o fundamento.',
);
if (e.status === 409) {
setNeedsRefresh(true);
void refresh();
}
} else if (
e instanceof PortalApiError &&
[401, 403, 419].includes(e.status)
) {
setBlocked(true);
setError(
'Confirme a conta e renove a autenticação MFA antes de repetir. A preparação foi conservada.',
);
} else
setError(
e instanceof Error
? e.message
: 'Resultado sem confirmação. Conserve a tentativa original.',
);
} finally {
if (
alive.current &&
epoch.current === scope &&
active.current === attempt
) {
active.current = null;
setBusy(false);
}
}
}
function adopt() {
if (!valid || busy || refreshing || pending.current || blocked) return;
setBase(plan);
setConfirmed(false);
setNeedsRefresh(false);
setMessage(
'Prévia actual confirmada. Reveja o fundamento e confirme esta operação.',
);
setError('');
}
usePagePreparationGuard(
!!reason || uncertain,
'Existe um fundamento preparado ou uma instalação sem confirmação. Sair desta página?',
);
return {
plan,
base,
reason,
change,
confirmed,
setConfirmed,
busy,
refreshing,
uncertain,
message,
error,
stale,
refresh,
send,
adopt,
canAdopt:
valid &&
!busy &&
!refreshing &&
!pending.current &&
!blocked &&
stale,
canRetry:
valid &&
plan.mfa_confirmed &&
!busy &&
!refreshing &&
!blocked &&
!!pending.current,
canSend:
valid &&
!blocked &&
!busy &&
!refreshing &&
!needsRefresh &&
!stale &&
!uncertain &&
confirmed &&
plan.ready &&
!plan.installed &&
plan.mfa_confirmed &&
(plan.consistent || plan.repairable),
};
}