mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-17 17:42:19 +00:00
Update vpn service
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
import {
|
||||
ShieldCheck, Globe, ExternalLink, Loader2, CheckCircle, XCircle,
|
||||
Play, Square, RotateCw, Trash2, FileText, ChevronRight, ChevronDown,
|
||||
AlertTriangle, Info, Network, Eye, EyeOff, Settings, Wifi,
|
||||
AlertTriangle, Info, Network, Eye, EyeOff, Settings, Wifi, Key,
|
||||
} from "lucide-react"
|
||||
import { fetchApi } from "../lib/api-config"
|
||||
|
||||
@@ -96,6 +96,12 @@ export function SecureGatewaySetup() {
|
||||
// Host IP for "Host Only" mode
|
||||
const [hostIp, setHostIp] = useState("")
|
||||
|
||||
// Update Auth Key
|
||||
const [showUpdateAuthKey, setShowUpdateAuthKey] = useState(false)
|
||||
const [newAuthKey, setNewAuthKey] = useState("")
|
||||
const [updateAuthKeyLoading, setUpdateAuthKeyLoading] = useState(false)
|
||||
const [updateAuthKeyError, setUpdateAuthKeyError] = useState("")
|
||||
|
||||
// Password visibility
|
||||
const [visiblePasswords, setVisiblePasswords] = useState<Set<string>>(new Set())
|
||||
|
||||
@@ -139,10 +145,13 @@ export function SecureGatewaySetup() {
|
||||
const networksRes = await fetchApi("/api/oci/networks")
|
||||
if (networksRes.success) {
|
||||
setNetworks(networksRes.networks || [])
|
||||
// Get host IP for "Host Only" mode
|
||||
// Get host IP for "Host Only" mode - extract just the IP without CIDR
|
||||
const primaryNetwork = networksRes.networks?.find((n: NetworkInfo) => n.recommended) || networksRes.networks?.[0]
|
||||
if (primaryNetwork?.address) {
|
||||
setHostIp(primaryNetwork.address)
|
||||
// Remove CIDR notation if present (e.g., "192.168.0.55/24" -> "192.168.0.55")
|
||||
const ip = primaryNetwork.address.split("/")[0]
|
||||
setHostIp(ip)
|
||||
console.log("[v0] Host IP for Host Only mode:", ip)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -187,6 +196,8 @@ export function SecureGatewaySetup() {
|
||||
if (config.access_mode === "host_only" && hostIp) {
|
||||
deployConfig.advertise_routes = [`${hostIp}/32`]
|
||||
}
|
||||
|
||||
console.log("[v0] Deploy config:", JSON.stringify(deployConfig, null, 2))
|
||||
|
||||
setDeployProgress("Creating LXC container...")
|
||||
|
||||
@@ -218,10 +229,15 @@ export function SecureGatewaySetup() {
|
||||
setDeploying(false)
|
||||
setCurrentStep(0)
|
||||
|
||||
// Show post-deploy confirmation if user needs to approve routes
|
||||
const needsApproval = deployConfig.advertise_routes?.length > 0 || deployConfig.exit_node || deployConfig.accept_routes
|
||||
// Show post-deploy confirmation - always show when access mode is set (routes need approval)
|
||||
const needsApproval = deployConfig.access_mode && deployConfig.access_mode !== "none"
|
||||
if (needsApproval) {
|
||||
setDeployedConfig(deployConfig)
|
||||
// Ensure advertise_routes is set for the dialog
|
||||
const finalConfig = { ...deployConfig }
|
||||
if (deployConfig.access_mode === "host_only" && hostIp) {
|
||||
finalConfig.advertise_routes = [`${hostIp}/32`]
|
||||
}
|
||||
setDeployedConfig(finalConfig)
|
||||
setShowPostDeployInfo(true)
|
||||
}
|
||||
}, 2000)
|
||||
@@ -248,6 +264,40 @@ export function SecureGatewaySetup() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateAuthKey = async () => {
|
||||
if (!newAuthKey.trim()) {
|
||||
setUpdateAuthKeyError("Auth Key is required")
|
||||
return
|
||||
}
|
||||
|
||||
setUpdateAuthKeyLoading(true)
|
||||
setUpdateAuthKeyError("")
|
||||
|
||||
try {
|
||||
const result = await fetchApi("/api/oci/installed/secure-gateway/update-auth-key", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
auth_key: newAuthKey.trim()
|
||||
})
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
setUpdateAuthKeyError(result.message || "Failed to update auth key")
|
||||
setUpdateAuthKeyLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Success - close dialog and reload status
|
||||
setShowUpdateAuthKey(false)
|
||||
setNewAuthKey("")
|
||||
await loadStatus()
|
||||
} catch (err: any) {
|
||||
setUpdateAuthKeyError(err.message || "Failed to update auth key")
|
||||
} finally {
|
||||
setUpdateAuthKeyLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = async () => {
|
||||
setActionLoading("remove")
|
||||
try {
|
||||
@@ -588,15 +638,18 @@ export function SecureGatewaySetup() {
|
||||
</div>
|
||||
|
||||
{/* Approval notice */}
|
||||
{(config.access_mode !== "none" || config.exit_node) && !deploying && (
|
||||
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-3">
|
||||
{(config.access_mode && config.access_mode !== "none") && !deploying && (
|
||||
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-3 space-y-2">
|
||||
<p className="text-xs text-cyan-400 flex items-start gap-2">
|
||||
<Info className="h-4 w-4 flex-shrink-0 mt-0.5" />
|
||||
<span>
|
||||
After deployment, you{"'"}ll need to <strong>approve the subnet routes</strong>
|
||||
{config.exit_node && <span> and <strong>exit node</strong></span>} in your Tailscale Admin Console for them to work.
|
||||
<strong>Important:</strong> After deployment, you must approve the subnet route in Tailscale Admin for remote access to work.
|
||||
{config.exit_node && <span> You{"'"}ll also need to approve the exit node.</span>}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground ml-6">
|
||||
We{"'"}ll show you exactly what to do after the gateway is deployed.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -760,8 +813,18 @@ export function SecureGatewaySetup() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tailscale admin link */}
|
||||
{/* Update Auth Key button */}
|
||||
<div className="pt-2 border-t border-border flex items-center justify-between">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setShowUpdateAuthKey(true)}
|
||||
disabled={actionLoading !== null}
|
||||
className="text-xs h-7 px-2"
|
||||
>
|
||||
<Key className="h-3 w-3 mr-1" />
|
||||
Update Auth Key
|
||||
</Button>
|
||||
<a
|
||||
href="https://login.tailscale.com/admin/machines"
|
||||
target="_blank"
|
||||
@@ -830,6 +893,75 @@ export function SecureGatewaySetup() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Update Auth Key Dialog */}
|
||||
<Dialog open={showUpdateAuthKey} onOpenChange={(open) => {
|
||||
setShowUpdateAuthKey(open)
|
||||
if (!open) {
|
||||
setNewAuthKey("")
|
||||
setUpdateAuthKeyError("")
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Key className="h-5 w-5 text-cyan-500" />
|
||||
Update Auth Key
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a new Tailscale auth key to re-authenticate the gateway. This is useful if your previous key has expired.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">New Auth Key</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={newAuthKey}
|
||||
onChange={(e) => setNewAuthKey(e.target.value)}
|
||||
placeholder="tskey-auth-..."
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Generate a new key at{" "}
|
||||
<a
|
||||
href="https://login.tailscale.com/admin/settings/keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-cyan-500 hover:text-cyan-400 underline"
|
||||
>
|
||||
Tailscale Admin > Settings > Keys
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{updateAuthKeyError && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
|
||||
<p className="text-xs text-red-500">{updateAuthKeyError}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="outline" onClick={() => setShowUpdateAuthKey(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdateAuthKey}
|
||||
disabled={updateAuthKeyLoading || !newAuthKey.trim()}
|
||||
className="bg-cyan-600 hover:bg-cyan-700"
|
||||
>
|
||||
{updateAuthKeyLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Update Key
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Post-Deploy Info Dialog */}
|
||||
<Dialog open={showPostDeployInfo} onOpenChange={setShowPostDeployInfo}>
|
||||
<DialogContent className="max-w-md">
|
||||
@@ -880,12 +1012,24 @@ export function SecureGatewaySetup() {
|
||||
|
||||
<div className="bg-muted/30 rounded-lg p-4 space-y-2">
|
||||
<p className="text-sm font-medium">How to approve:</p>
|
||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
<li>Go to Tailscale Admin Console</li>
|
||||
<li>Find the machine "{deployedConfig.hostname || "proxmox-gateway"}"</li>
|
||||
<li>Click on it and approve the pending routes/exit node</li>
|
||||
<ol className="text-sm text-muted-foreground space-y-2 list-decimal list-inside">
|
||||
<li>Click the button below to open Tailscale Admin</li>
|
||||
<li>Find <span className="font-mono text-cyan-400">{deployedConfig.hostname || "proxmox-gateway"}</span> in the machines list</li>
|
||||
<li>Click on it to open machine details</li>
|
||||
<li>In the <strong>Subnets</strong> section, click <strong>Edit</strong> and enable the route</li>
|
||||
{deployedConfig.exit_node && (
|
||||
<li>In <strong>Routing Settings</strong>, enable <strong>Exit Node</strong></li>
|
||||
)}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3">
|
||||
<p className="text-xs text-green-400">
|
||||
Once approved, you can access your Proxmox host at{" "}
|
||||
<span className="font-mono">{deployedConfig.advertise_routes?.[0]?.replace("/32", "") || hostIp}:8006</span> (Proxmox UI) or{" "}
|
||||
<span className="font-mono">{deployedConfig.advertise_routes?.[0]?.replace("/32", "") || hostIp}:8008</span> (ProxMenux Monitor) from any device with Tailscale.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
|
||||
@@ -475,3 +475,48 @@ def get_app_status(app_id: str):
|
||||
"success": False,
|
||||
"message": str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@oci_bp.route("/installed/<app_id>/update-auth-key", methods=["POST"])
|
||||
@require_auth
|
||||
def update_auth_key(app_id: str):
|
||||
"""
|
||||
Update the Tailscale auth key for an installed gateway.
|
||||
|
||||
This is useful when the auth key expires and the gateway needs to re-authenticate.
|
||||
|
||||
Body:
|
||||
{
|
||||
"auth_key": "tskey-auth-xxx"
|
||||
}
|
||||
|
||||
Returns:
|
||||
Success status and message.
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data or "auth_key" not in data:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "auth_key is required in request body"
|
||||
}), 400
|
||||
|
||||
auth_key = data["auth_key"]
|
||||
|
||||
if not auth_key.startswith("tskey-"):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Invalid auth key format. Should start with 'tskey-'"
|
||||
}), 400
|
||||
|
||||
result = oci_manager.update_auth_key(app_id, auth_key)
|
||||
status_code = 200 if result.get("success") else 400
|
||||
return jsonify(result), status_code
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update auth key: {e}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": str(e)
|
||||
}), 500
|
||||
|
||||
@@ -1111,6 +1111,82 @@ def detect_networks() -> List[Dict[str, str]]:
|
||||
return networks
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Update Auth Key (for Tailscale re-authentication)
|
||||
# =================================================================
|
||||
def update_auth_key(app_id: str, auth_key: str) -> Dict[str, Any]:
|
||||
"""Update the Tailscale auth key for a running gateway."""
|
||||
result = {"success": False, "message": "", "app_id": app_id}
|
||||
|
||||
# Get VMID for the app
|
||||
vmid = _get_vmid_for_app(app_id)
|
||||
if not vmid:
|
||||
result["message"] = f"App {app_id} not found or not installed"
|
||||
return result
|
||||
|
||||
# Check if container is running
|
||||
status = get_app_status(app_id)
|
||||
if status.get("state") != "running":
|
||||
result["message"] = "Container must be running to update auth key"
|
||||
return result
|
||||
|
||||
logger.info(f"Updating auth key for {app_id} (VMID: {vmid})")
|
||||
print(f"[*] Updating auth key for {app_id}...")
|
||||
|
||||
# Run tailscale logout first to clear existing state
|
||||
print(f"[*] Logging out of Tailscale...")
|
||||
_run_pve_cmd(["pct", "exec", str(vmid), "--", "tailscale", "logout"], timeout=30)
|
||||
|
||||
# Wait a moment for logout to complete
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
# Run tailscale up with new auth key
|
||||
print(f"[*] Authenticating with new key...")
|
||||
|
||||
# Load saved config to get original settings
|
||||
config_file = os.path.join(INSTANCES_DIR, app_id, "config.json")
|
||||
config = {}
|
||||
if os.path.exists(config_file):
|
||||
try:
|
||||
with open(config_file) as f:
|
||||
saved_config = json.load(f)
|
||||
config = saved_config.get("values", {})
|
||||
except:
|
||||
pass
|
||||
|
||||
# Build tailscale up command
|
||||
ts_cmd = ["tailscale", "up", f"--authkey={auth_key}"]
|
||||
|
||||
hostname = config.get("hostname")
|
||||
if hostname:
|
||||
ts_cmd.append(f"--hostname={hostname}")
|
||||
|
||||
advertise_routes = config.get("advertise_routes")
|
||||
if advertise_routes:
|
||||
if isinstance(advertise_routes, list):
|
||||
advertise_routes = ",".join(advertise_routes)
|
||||
ts_cmd.append(f"--advertise-routes={advertise_routes}")
|
||||
|
||||
if config.get("exit_node"):
|
||||
ts_cmd.append("--advertise-exit-node")
|
||||
|
||||
if config.get("accept_routes"):
|
||||
ts_cmd.append("--accept-routes")
|
||||
|
||||
rc, out, err = _run_pve_cmd(["pct", "exec", str(vmid), "--"] + ts_cmd, timeout=60)
|
||||
|
||||
if rc != 0:
|
||||
logger.error(f"Failed to update auth key: {err}")
|
||||
result["message"] = f"Failed to authenticate: {err}"
|
||||
return result
|
||||
|
||||
print(f"[OK] Auth key updated successfully")
|
||||
result["success"] = True
|
||||
result["message"] = "Auth key updated successfully"
|
||||
return result
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Runtime Detection (for backward compatibility)
|
||||
# =================================================================
|
||||
|
||||
Reference in New Issue
Block a user