Add OpenWRT SSH provisioning and readiness workflow
This commit is contained in:
+245
-12
@@ -1,5 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { RouterItem } from "../types/router";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
@@ -17,6 +19,14 @@ type DetectionResult = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
type PrepareResult = {
|
||||
success: boolean;
|
||||
hostname_updated: boolean;
|
||||
password_updated: boolean;
|
||||
message: string;
|
||||
logs: string;
|
||||
};
|
||||
|
||||
type CompatibilityResult = {
|
||||
compatible: boolean;
|
||||
version: string | null;
|
||||
@@ -44,7 +54,11 @@ type SshResult = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function OpenWrtConfigPage() {
|
||||
type Props = {
|
||||
routers: RouterItem[];
|
||||
};
|
||||
|
||||
export function OpenWrtConfigPage({ routers }: Props) {
|
||||
const [detecting, setDetecting] = useState(false);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [detection, setDetection] = useState<DetectionResult | null>(null);
|
||||
@@ -53,6 +67,13 @@ export function OpenWrtConfigPage() {
|
||||
const [host, setHost] = useState("");
|
||||
const [username, setUsername] = useState("root");
|
||||
const [password, setPassword] = useState("");
|
||||
const [selectedRouterId, setSelectedRouterId] = useState("");
|
||||
const [newRootPassword, setNewRootPassword] = useState("");
|
||||
const [confirmRootPassword, setConfirmRootPassword] = useState("");
|
||||
const [routerDropdownOpen, setRouterDropdownOpen] = useState(false);
|
||||
|
||||
const [preparing, setPreparing] = useState(false);
|
||||
const [prepareResult, setPrepareResult] = useState<PrepareResult | null>(null);
|
||||
|
||||
const compatibility = sshResult?.compatibility;
|
||||
|
||||
@@ -73,6 +94,46 @@ export function OpenWrtConfigPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const sshAuthFailed =
|
||||
sshResult &&
|
||||
!sshResult.connected &&
|
||||
sshResult.message.toLowerCase().includes("permission denied");
|
||||
|
||||
async function prepareRouter() {
|
||||
if (!selectedRouter || !targetHostname) return;
|
||||
|
||||
try {
|
||||
setPreparing(true);
|
||||
setPrepareResult(null);
|
||||
|
||||
const result = await invoke<PrepareResult>("prepare_openwrt_router", {
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
targetHostname,
|
||||
newPassword: newRootPassword,
|
||||
});
|
||||
|
||||
setPrepareResult(result);
|
||||
|
||||
if (result.success) {
|
||||
setPassword(newRootPassword);
|
||||
setNewRootPassword("");
|
||||
setConfirmRootPassword("");
|
||||
|
||||
const refreshed = await invoke<SshResult>("test_openwrt_ssh", {
|
||||
host,
|
||||
username,
|
||||
password: newRootPassword,
|
||||
});
|
||||
|
||||
setSshResult(refreshed);
|
||||
}
|
||||
} finally {
|
||||
setPreparing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function connectSsh() {
|
||||
try {
|
||||
setConnecting(true);
|
||||
@@ -89,6 +150,14 @@ export function OpenWrtConfigPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const selectedRouter = routers.find((router) => router.id === selectedRouterId);
|
||||
|
||||
const vpnIpParts = selectedRouter?.vpnIp?.split(".") ?? [];
|
||||
|
||||
const targetHostname = selectedRouter?.vpnIp
|
||||
? `Litoral_Regas_${vpnIpParts[vpnIpParts.length - 1]}`
|
||||
: "";
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="page-header">
|
||||
@@ -161,19 +230,31 @@ export function OpenWrtConfigPage() {
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
|
||||
if (sshResult && !sshResult.connected) {
|
||||
setSshResult(null);
|
||||
}
|
||||
}}
|
||||
placeholder="Password SSH"
|
||||
/>
|
||||
{sshAuthFailed && (
|
||||
<span className="form-error">
|
||||
Password SSH inválida ou em falta. Introduza uma password válida.
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
|
||||
<button
|
||||
className="primary"
|
||||
onClick={connectSsh}
|
||||
disabled={connecting || !host || !username}
|
||||
>
|
||||
<ShieldCheck size={16} />
|
||||
{connecting ? "A ligar..." : "Ligar ao Router"}
|
||||
</button>
|
||||
<div className="openwrt-button-wrap">
|
||||
<button
|
||||
className="primary"
|
||||
onClick={connectSsh}
|
||||
disabled={connecting || !host || !username}
|
||||
>
|
||||
<ShieldCheck size={16} />
|
||||
{connecting ? "A ligar..." : "Ligar ao Router"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -199,7 +280,11 @@ export function OpenWrtConfigPage() {
|
||||
{!sshResult.connected && (
|
||||
<div className="status-note">
|
||||
<XCircle size={24} />
|
||||
<p>{sshResult.message}</p>
|
||||
<p>
|
||||
{sshAuthFailed
|
||||
? "Não foi possível autenticar. Verifique a password SSH."
|
||||
: sshResult.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -311,8 +396,156 @@ export function OpenWrtConfigPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sshResult?.connected && sshResult.readiness && (
|
||||
<div className="dashboard-card wide-panel">
|
||||
<div className="card-header">
|
||||
<div>
|
||||
<h2>Preparação do Router</h2>
|
||||
<p>Associe este router físico a um router da plataforma</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="openwrt-prepare-form">
|
||||
<label>
|
||||
Router da Plataforma
|
||||
<div className="custom-select">
|
||||
<button
|
||||
type="button"
|
||||
className="custom-select-button"
|
||||
onClick={() => setRouterDropdownOpen((prev) => !prev)}
|
||||
>
|
||||
<span>
|
||||
{selectedRouter
|
||||
? `${selectedRouter.name} - ${selectedRouter.vpnIp}`
|
||||
: "Selecionar router..."}
|
||||
</span>
|
||||
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={`chevron ${routerDropdownOpen ? "open" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{routerDropdownOpen && (
|
||||
<div className="custom-select-menu">
|
||||
{routers.map((router) => (
|
||||
<button
|
||||
key={router.id}
|
||||
type="button"
|
||||
className="custom-select-option"
|
||||
onClick={() => {
|
||||
setSelectedRouterId(router.id);
|
||||
setRouterDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
{router.name} - {router.vpnIp}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Hostname Gerado
|
||||
<input value={targetHostname || "-"} readOnly />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Nova Password Root
|
||||
<input
|
||||
type="password"
|
||||
value={newRootPassword}
|
||||
onChange={(e) => setNewRootPassword(e.target.value)}
|
||||
placeholder="Nova password"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Confirmar Password Root
|
||||
<input
|
||||
type="password"
|
||||
value={confirmRootPassword}
|
||||
onChange={(e) => setConfirmRootPassword(e.target.value)}
|
||||
placeholder="Confirmar password"
|
||||
/>
|
||||
|
||||
{confirmRootPassword &&
|
||||
newRootPassword !== confirmRootPassword && (
|
||||
<span className="form-error">
|
||||
As passwords não coincidem.
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{selectedRouter && (
|
||||
<div className="detail-list">
|
||||
<DetailRow label="Router selecionado" value={selectedRouter.name} />
|
||||
<DetailRow label="IP VPN" value={selectedRouter.vpnIp || "-"} />
|
||||
<DetailRow label="Hostname final" value={targetHostname} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="primary"
|
||||
onClick={prepareRouter}
|
||||
disabled={
|
||||
preparing ||
|
||||
!selectedRouter ||
|
||||
!targetHostname ||
|
||||
!newRootPassword ||
|
||||
newRootPassword !== confirmRootPassword
|
||||
}
|
||||
>
|
||||
{preparing ? "A preparar..." : "Preparar Router"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{prepareResult && (
|
||||
<div className="dashboard-card wide-panel">
|
||||
<div className="card-header">
|
||||
<div>
|
||||
<h2>Resultado da Preparação</h2>
|
||||
<p>Estado da configuração inicial do router</p>
|
||||
</div>
|
||||
|
||||
<span className={`badge ${prepareResult.success ? "success" : "failed"}`}>
|
||||
{prepareResult.success ? "Sucesso" : "Falhou"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="detail-list">
|
||||
<DetailRow
|
||||
label="Hostname"
|
||||
value={prepareResult.hostname_updated ? "Atualizado" : "Falhou"}
|
||||
/>
|
||||
|
||||
<DetailRow
|
||||
label="Password Root"
|
||||
value={prepareResult.password_updated ? "Atualizada" : "Falhou"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="status-note">
|
||||
{prepareResult.success ? <CheckCircle2 size={24} /> : <XCircle size={24} />}
|
||||
<p>{prepareResult.message}</p>
|
||||
</div>
|
||||
|
||||
{prepareResult.logs && (
|
||||
<div className="log-box">
|
||||
<pre>{prepareResult.logs}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user