Compare commits
38 Commits
update_cor
...
fix/465
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db12ac4219 | ||
|
|
f66d1db861 | ||
|
|
886c3e37ff | ||
|
|
38deb09aa9 | ||
|
|
6d326dce1f | ||
|
|
6c8e177d3e | ||
|
|
879a548345 | ||
|
|
64cd81d5ba | ||
|
|
61e75949c8 | ||
|
|
a5d24bfad7 | ||
|
|
04595c0093 | ||
|
|
06fdb4889d | ||
|
|
38d4f9f918 | ||
|
|
63dc7c6983 | ||
|
|
d57c6059fc | ||
|
|
eb152f9fae | ||
|
|
1a8e98fec0 | ||
|
|
83a1c7ea31 | ||
|
|
79c63a7d3d | ||
|
|
753721eee0 | ||
|
|
09607296af | ||
|
|
c88040084a | ||
|
|
2573eb7314 | ||
|
|
414c356446 | ||
|
|
c38ded7a39 | ||
|
|
0cfed84cd0 | ||
|
|
9611bc9bcf | ||
|
|
6fe2a790fd | ||
|
|
5ea71837e7 | ||
|
|
bf5ebc72b6 | ||
|
|
a32c7bcbba | ||
|
|
98c6e79db6 | ||
|
|
c962a9cd5a | ||
|
|
5d20a6d694 | ||
|
|
cb4e8c543a | ||
|
|
2ba213de49 | ||
|
|
849aabb575 | ||
|
|
dd33df2033 |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -4,7 +4,7 @@
|
||||
|
||||
|
||||
## 🔗 Related PR / Issue
|
||||
Link: #
|
||||
Fixes: #
|
||||
|
||||
|
||||
## ✅ Prerequisites (**X** in brackets)
|
||||
|
||||
895
package-lock.json
generated
895
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@@ -25,33 +25,33 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/adapter-better-sqlite3": "^7.1.0",
|
||||
"@prisma/client": "^7.1.0",
|
||||
"@prisma/adapter-better-sqlite3": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@t3-oss/env-nextjs": "^0.13.10",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@trpc/client": "^11.8.0",
|
||||
"@tanstack/react-query": "^5.90.18",
|
||||
"@trpc/client": "^11.8.1",
|
||||
"@trpc/react-query": "^11.8.1",
|
||||
"@trpc/server": "^11.8.0",
|
||||
"@trpc/server": "^11.8.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"better-sqlite3": "^12.6.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cron-validator": "^1.4.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"lucide-react": "^0.561.0",
|
||||
"next": "^16.0.10",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.3",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-pty": "^1.0.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -62,37 +62,37 @@
|
||||
"strip-ansi": "^7.1.2",
|
||||
"superjson": "^2.2.6",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"ws": "^8.18.3",
|
||||
"zod": "^4.1.13"
|
||||
"ws": "^8.19.0",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.10.4",
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.15",
|
||||
"@vitest/ui": "^4.0.14",
|
||||
"baseline-browser-mapping": "^2.9.3",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.1.0",
|
||||
"jsdom": "^27.3.0",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"@vitest/ui": "^4.0.17",
|
||||
"baseline-browser-mapping": "^2.9.15",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-next": "^16.1.3",
|
||||
"jsdom": "^27.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier": "^3.8.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"prisma": "^7.1.0",
|
||||
"prisma": "^7.2.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"vitest": "^4.0.14"
|
||||
"typescript-eslint": "^8.53.0",
|
||||
"vitest": "^4.0.17"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.39.3"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: tteck (tteckster)
|
||||
# Co-Author: MickLesk
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: michelroegl-brunner
|
||||
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: community-scripts ORG
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE
|
||||
# Revision: 1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
||||
|
||||
# ==============================================================================
|
||||
@@ -123,6 +123,7 @@ icons() {
|
||||
CREATING="${TAB}🚀${TAB}${CL}"
|
||||
ADVANCED="${TAB}🧩${TAB}${CL}"
|
||||
FUSE="${TAB}🗂️${TAB}${CL}"
|
||||
GPU="${TAB}🎮${TAB}${CL}"
|
||||
HOURGLASS="${TAB}⏳${TAB}"
|
||||
}
|
||||
|
||||
@@ -808,18 +809,15 @@ cleanup_lxc() {
|
||||
find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true
|
||||
find /tmp /var/tmp -type f -name 'tempfile*' -delete 2>/dev/null || true
|
||||
|
||||
# Truncate writable log files silently (permission errors ignored)
|
||||
if command -v truncate >/dev/null 2>&1; then
|
||||
find /var/log -type f -writable -print0 2>/dev/null |
|
||||
xargs -0 -n1 truncate -s 0 2>/dev/null || true
|
||||
# Node.js npm - directly remove cache directory
|
||||
# npm cache clean/verify can fail with ENOTEMPTY errors, so we skip them
|
||||
if command -v npm &>/dev/null; then
|
||||
rm -rf /root/.npm/_cacache /root/.npm/_logs 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Node.js npm
|
||||
if command -v npm &>/dev/null; then $STD npm cache clean --force || true; fi
|
||||
# Node.js yarn
|
||||
if command -v yarn &>/dev/null; then $STD yarn cache clean || true; fi
|
||||
if command -v yarn &>/dev/null; then yarn cache clean &>/dev/null || true; fi
|
||||
# Node.js pnpm
|
||||
if command -v pnpm &>/dev/null; then $STD pnpm store prune || true; fi
|
||||
if command -v pnpm &>/dev/null; then pnpm store prune &>/dev/null || true; fi
|
||||
# Go
|
||||
if command -v go &>/dev/null; then $STD go clean -cache -modcache || true; fi
|
||||
# Rust cargo
|
||||
@@ -827,11 +825,8 @@ cleanup_lxc() {
|
||||
# Ruby gem
|
||||
if command -v gem &>/dev/null; then $STD gem cleanup || true; fi
|
||||
# Composer (PHP)
|
||||
if command -v composer &>/dev/null; then $STD composer clear-cache || true; fi
|
||||
if command -v composer &>/dev/null; then COMPOSER_ALLOW_SUPERUSER=1 $STD composer clear-cache || true; fi
|
||||
|
||||
if command -v journalctl &>/dev/null; then
|
||||
$STD journalctl --vacuum-time=10m || true
|
||||
fi
|
||||
msg_ok "Cleaned"
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -34,9 +34,9 @@
|
||||
# * Node.js/npm errors (243-249, 254)
|
||||
# * Python/pip/uv errors (210-212)
|
||||
# * PostgreSQL errors (231-234)
|
||||
# * MySQL/MariaDB errors (260-263)
|
||||
# * MongoDB errors (251-253)
|
||||
# * Proxmox custom codes (200-209, 213-223, 225)
|
||||
# * MySQL/MariaDB errors (241-244)
|
||||
# * MongoDB errors (251-254)
|
||||
# * Proxmox custom codes (200-231)
|
||||
# - Returns description string for given exit code
|
||||
# ------------------------------------------------------------------------------
|
||||
explain_exit_code() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: tteck (tteckster)
|
||||
# Co-Author: MickLesk
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
@@ -222,21 +222,12 @@ motd_ssh() {
|
||||
# Set terminal to 256-color mode
|
||||
grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc
|
||||
|
||||
# Get OS information (Debian / Ubuntu)
|
||||
if [ -f "/etc/os-release" ]; then
|
||||
OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"')
|
||||
OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')
|
||||
elif [ -f "/etc/debian_version" ]; then
|
||||
OS_NAME="Debian"
|
||||
OS_VERSION=$(cat /etc/debian_version)
|
||||
fi
|
||||
|
||||
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
|
||||
echo "echo -e \"\"" >"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE"
|
||||
echo "echo \"\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
source "$SCRIPT_DIR/../core/build.func"
|
||||
# Copyright (c) 2021-2025 tteck
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.debian.org/
|
||||
@@ -40,5 +40,5 @@ start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
msg_ok "Completed successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2025 tteck
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.debian.org/
|
||||
|
||||
@@ -1610,6 +1610,7 @@ class ScriptExecutionHandler {
|
||||
// TerminalHandler removed - not used by current application
|
||||
|
||||
app.prepare().then(() => {
|
||||
console.log('> Next.js app prepared successfully');
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
try {
|
||||
// Be sure to pass `true` as the second argument to `url.parse`.
|
||||
@@ -1715,4 +1716,9 @@ app.prepare().then(() => {
|
||||
autoSyncModule.setupGracefulShutdown();
|
||||
}
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error('> Failed to start server:', err.message);
|
||||
console.error('> If you see "Could not find a production build", run: npm run build');
|
||||
console.error('> Full error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -58,6 +58,11 @@ export function ConfigurationModal({
|
||||
// Advanced mode state
|
||||
const [advancedVars, setAdvancedVars] = useState<EnvVars>({});
|
||||
|
||||
// Discovered SSH keys on the Proxmox host (advanced mode only)
|
||||
const [discoveredSshKeys, setDiscoveredSshKeys] = useState<string[]>([]);
|
||||
const [discoveredSshKeysLoading, setDiscoveredSshKeysLoading] = useState(false);
|
||||
const [discoveredSshKeysError, setDiscoveredSshKeysError] = useState<string | null>(null);
|
||||
|
||||
// Validation errors
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -119,6 +124,38 @@ export function ConfigurationModal({
|
||||
}
|
||||
}, [actualScript, server, mode, resources, slug]);
|
||||
|
||||
// Discover SSH keys on the Proxmox host when advanced mode is open
|
||||
useEffect(() => {
|
||||
if (!server?.id || !isOpen || mode !== 'advanced') {
|
||||
setDiscoveredSshKeys([]);
|
||||
setDiscoveredSshKeysError(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setDiscoveredSshKeysLoading(true);
|
||||
setDiscoveredSshKeysError(null);
|
||||
fetch(`/api/servers/${server.id}/discover-ssh-keys`)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(res.status === 404 ? 'Server not found' : res.statusText);
|
||||
return res.json();
|
||||
})
|
||||
.then((data: { keys?: string[] }) => {
|
||||
if (!cancelled && Array.isArray(data.keys)) setDiscoveredSshKeys(data.keys);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
setDiscoveredSshKeys([]);
|
||||
setDiscoveredSshKeysError(err instanceof Error ? err.message : 'Could not detect keys');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setDiscoveredSshKeysLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [server?.id, isOpen, mode]);
|
||||
|
||||
// Validation functions
|
||||
const validateIPv4 = (ip: string): boolean => {
|
||||
if (!ip) return true; // Empty is allowed (auto)
|
||||
@@ -275,6 +312,16 @@ export function ConfigurationModal({
|
||||
if ((hasPassword || hasSSHKey) && envVars.var_ssh !== 'no') {
|
||||
envVars.var_ssh = 'yes';
|
||||
}
|
||||
|
||||
// Normalize var_tags: accept both comma and semicolon, output comma-separated
|
||||
const rawTags = envVars.var_tags;
|
||||
if (typeof rawTags === 'string' && rawTags.trim() !== '') {
|
||||
envVars.var_tags = rawTags
|
||||
.split(/[,;]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty string values (but keep 0, false, etc.)
|
||||
@@ -644,13 +691,13 @@ export function ConfigurationModal({
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Tags (comma-separated)
|
||||
Tags (comma or semicolon separated)
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_tags === 'boolean' ? '' : String(advancedVars.var_tags ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_tags', e.target.value)}
|
||||
placeholder="community-script"
|
||||
placeholder="e.g. tag1; tag2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -677,11 +724,40 @@ export function ConfigurationModal({
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
SSH Authorized Key
|
||||
</label>
|
||||
{discoveredSshKeysLoading && (
|
||||
<p className="text-sm text-muted-foreground mb-2">Detecting SSH keys...</p>
|
||||
)}
|
||||
{discoveredSshKeysError && !discoveredSshKeysLoading && (
|
||||
<p className="text-sm text-muted-foreground mb-2">Could not detect keys on host</p>
|
||||
)}
|
||||
{discoveredSshKeys.length > 0 && !discoveredSshKeysLoading && (
|
||||
<div className="mb-2">
|
||||
<label htmlFor="discover-ssh-key" className="sr-only">Use detected key</label>
|
||||
<select
|
||||
id="discover-ssh-key"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none mb-2"
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
const idx = e.target.value;
|
||||
if (idx === '') return;
|
||||
const key = discoveredSshKeys[Number(idx)];
|
||||
if (key) updateAdvancedVar('var_ssh_authorized_key', key);
|
||||
}}
|
||||
>
|
||||
<option value="">— Select or paste below —</option>
|
||||
{discoveredSshKeys.map((key, i) => (
|
||||
<option key={i} value={i}>
|
||||
{key.length > 44 ? `${key.slice(0, 44)}...` : key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_ssh_authorized_key === 'boolean' ? '' : String(advancedVars.var_ssh_authorized_key ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_ssh_authorized_key', e.target.value)}
|
||||
placeholder="ssh-rsa AAAA..."
|
||||
placeholder="Or paste a public key: ssh-rsa AAAA..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function Footer({ onOpenReleaseNotes }: FooterProps) {
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>© 2024 PVE Scripts Local</span>
|
||||
<span>© 2026 PVE Scripts Local</span>
|
||||
{versionData?.success && versionData.version && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
96
src/app/api/servers/[id]/discover-ssh-keys/route.ts
Normal file
96
src/app/api/servers/[id]/discover-ssh-keys/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../../../server/database-prisma';
|
||||
import { getSSHExecutionService } from '../../../../../server/ssh-execution-service';
|
||||
import type { Server } from '~/types/server';
|
||||
|
||||
const DISCOVER_TIMEOUT_MS = 10_000;
|
||||
|
||||
/** Match lines that look like SSH public keys (same as build.func) */
|
||||
const SSH_PUBKEY_RE = /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))\s+/;
|
||||
|
||||
/**
|
||||
* Run a command on the Proxmox host and return buffered stdout.
|
||||
* Resolves when the process exits or rejects on timeout/spawn error.
|
||||
*/
|
||||
function runRemoteCommand(
|
||||
server: Server,
|
||||
command: string,
|
||||
timeoutMs: number
|
||||
): Promise<{ stdout: string; exitCode: number }> {
|
||||
const ssh = getSSHExecutionService();
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: string[] = [];
|
||||
let settled = false;
|
||||
|
||||
const finish = (stdout: string, exitCode: number) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ stdout, exitCode });
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
reject(new Error('SSH discover keys timeout'));
|
||||
}, timeoutMs);
|
||||
|
||||
ssh
|
||||
.executeCommand(
|
||||
server,
|
||||
command,
|
||||
(data: string) => chunks.push(data),
|
||||
() => {},
|
||||
(code: number) => finish(chunks.join(''), code)
|
||||
)
|
||||
.catch((err) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id: idParam } = await params;
|
||||
const id = parseInt(idParam);
|
||||
if (isNaN(id)) {
|
||||
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
const server = await db.getServerById(id) as Server | null;
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Same paths as native build.func ssh_discover_default_files()
|
||||
const remoteScript = `bash -c 'for f in /root/.ssh/authorized_keys /root/.ssh/authorized_keys2 /root/.ssh/*.pub /etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/* 2>/dev/null; do [ -f "$f" ] && [ -r "$f" ] && grep -E "^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-)" "$f" 2>/dev/null; done | sort -u'`;
|
||||
|
||||
const { stdout } = await runRemoteCommand(server, remoteScript, DISCOVER_TIMEOUT_MS);
|
||||
|
||||
const keys = stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && SSH_PUBKEY_RE.test(line));
|
||||
|
||||
return NextResponse.json({ keys });
|
||||
} catch (error) {
|
||||
console.error('Error discovering SSH keys:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -238,6 +238,27 @@ export const versionRouter = createTRPCRouter({
|
||||
// Clear/create the log file
|
||||
await writeFile(logPath, '', 'utf-8');
|
||||
|
||||
// Always fetch the latest update.sh from GitHub before running
|
||||
// This ensures we always use the newest update script, avoiding
|
||||
// the "chicken-and-egg" problem where old scripts can't update properly
|
||||
const updateScriptUrl = 'https://raw.githubusercontent.com/community-scripts/ProxmoxVE-Local/main/update.sh';
|
||||
try {
|
||||
const response = await fetch(updateScriptUrl);
|
||||
if (response.ok) {
|
||||
const latestScript = await response.text();
|
||||
await writeFile(updateScriptPath, latestScript, { mode: 0o755 });
|
||||
// Log that we fetched the latest script
|
||||
await writeFile(logPath, '[INFO] Fetched latest update.sh from GitHub\n', { flag: 'a' });
|
||||
} else {
|
||||
// If fetch fails, log warning but continue with local script
|
||||
await writeFile(logPath, `[WARNING] Could not fetch latest update.sh (HTTP ${response.status}), using local version\n`, { flag: 'a' });
|
||||
}
|
||||
} catch (fetchError) {
|
||||
// If fetch fails, log warning but continue with local script
|
||||
const errorMsg = fetchError instanceof Error ? fetchError.message : 'Unknown error';
|
||||
await writeFile(logPath, `[WARNING] Could not fetch latest update.sh: ${errorMsg}, using local version\n`, { flag: 'a' });
|
||||
}
|
||||
|
||||
// Spawn the update script as a detached process using nohup
|
||||
// This allows it to run independently and kill the parent Node.js process
|
||||
// Redirect output to log file
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import 'dotenv/config'
|
||||
import { PrismaClient } from '../../prisma/generated/prisma/client.ts'
|
||||
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
|
||||
const globalForPrisma = globalThis;
|
||||
|
||||
// Ensure database directory exists before initializing Prisma
|
||||
// DATABASE_URL format: file:/path/to/database.db
|
||||
const dbUrl = process.env.DATABASE_URL || 'file:./data/settings.db';
|
||||
const dbPath = dbUrl.replace(/^file:/, '');
|
||||
const dbDir = dirname(dbPath);
|
||||
|
||||
if (!existsSync(dbDir)) {
|
||||
console.log(`Creating database directory: ${dbDir}`);
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL });
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import 'dotenv/config'
|
||||
import { PrismaClient } from '../../prisma/generated/prisma/client'
|
||||
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
|
||||
const globalForPrisma = globalThis as { prisma?: PrismaClient };
|
||||
|
||||
// Ensure database directory exists before initializing Prisma
|
||||
// DATABASE_URL format: file:/path/to/database.db
|
||||
const dbUrl = process.env.DATABASE_URL || 'file:./data/settings.db';
|
||||
const dbPath = dbUrl.replace(/^file:/, '');
|
||||
const dbDir = dirname(dbPath);
|
||||
|
||||
if (!existsSync(dbDir)) {
|
||||
console.log(`Creating database directory: ${dbDir}`);
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL! });
|
||||
|
||||
export const prisma: PrismaClient = globalForPrisma.prisma ?? new PrismaClient({
|
||||
|
||||
72
update.sh
72
update.sh
@@ -418,6 +418,12 @@ restore_backup_files() {
|
||||
verify_database_restored() {
|
||||
log "Verifying database was restored correctly..."
|
||||
|
||||
# Ensure data directory exists (will be auto-created by app if needed)
|
||||
if [ ! -d "data" ]; then
|
||||
log "Creating data directory..."
|
||||
mkdir -p data
|
||||
fi
|
||||
|
||||
# Check for both possible database filenames
|
||||
local db_file=""
|
||||
if [ -f "data/database.sqlite" ]; then
|
||||
@@ -425,8 +431,10 @@ verify_database_restored() {
|
||||
elif [ -f "data/settings.db" ]; then
|
||||
db_file="data/settings.db"
|
||||
else
|
||||
log_error "Database file not found after restore! (checked database.sqlite and settings.db)"
|
||||
return 1
|
||||
# Database doesn't exist yet - this is OK for new installations
|
||||
# The app will create it automatically via Prisma migrations
|
||||
log_warning "No existing database file found - will be created automatically on first start"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local db_size=$(stat -f%z "$db_file" 2>/dev/null || stat -c%s "$db_file" 2>/dev/null)
|
||||
@@ -481,11 +489,9 @@ check_service() {
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Stop the application before updating
|
||||
stop_application() {
|
||||
|
||||
|
||||
# Change to the application directory if we're not already there
|
||||
local app_dir
|
||||
if [ -f "package.json" ] && [ -f "server.js" ]; then
|
||||
@@ -624,7 +630,6 @@ update_files() {
|
||||
log_success "Application files updated successfully ($files_copied files)"
|
||||
}
|
||||
|
||||
|
||||
# Install dependencies and build
|
||||
install_and_build() {
|
||||
log "Installing dependencies..."
|
||||
@@ -705,11 +710,14 @@ install_and_build() {
|
||||
log "Building application..."
|
||||
# Set NODE_ENV to production for build
|
||||
export NODE_ENV=production
|
||||
# Unset TURBOPACK to prevent "Multiple bundler flags" error with --webpack
|
||||
unset TURBOPACK 2>/dev/null || true
|
||||
export TURBOPACK=''
|
||||
|
||||
# Create temporary file for npm build output
|
||||
local build_log="/tmp/npm_build_$$.log"
|
||||
|
||||
if ! npm run build > "$build_log" 2>&1; then
|
||||
if ! TURBOPACK='' npm run build >"$build_log" 2>&1; then
|
||||
log_error "Failed to build application"
|
||||
log_error "npm run build output:"
|
||||
cat "$build_log" | while read -r line; do
|
||||
@@ -776,6 +784,23 @@ start_with_npm() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Re-enable the systemd service on failure to prevent users from being locked out
|
||||
re_enable_service_on_failure() {
|
||||
if check_service; then
|
||||
log "Re-enabling systemd service after failure..."
|
||||
if systemctl enable pvescriptslocal.service 2>/dev/null; then
|
||||
log_success "Service re-enabled"
|
||||
if systemctl start pvescriptslocal.service 2>/dev/null; then
|
||||
log_success "Service started"
|
||||
else
|
||||
log_warning "Failed to start service - manual intervention may be required"
|
||||
fi
|
||||
else
|
||||
log_warning "Failed to re-enable service - manual intervention may be required"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Rollback function
|
||||
rollback() {
|
||||
log_warning "Rolling back to previous version..."
|
||||
@@ -847,6 +872,9 @@ rollback() {
|
||||
log_error "No backup directory found for rollback"
|
||||
fi
|
||||
|
||||
# Re-enable the service so users aren't locked out
|
||||
re_enable_service_on_failure
|
||||
|
||||
log_error "Update failed. Please check the logs and try again."
|
||||
exit 1
|
||||
}
|
||||
@@ -865,14 +893,14 @@ check_node_version() {
|
||||
|
||||
log "Detected Node.js version: $current"
|
||||
|
||||
if (( major_version < 24 )); then
|
||||
if ((major_version == 24)); then
|
||||
log_success "Node.js 24 already installed"
|
||||
elif ((major_version < 24)); then
|
||||
log_warning "Node.js < 24 detected → upgrading to Node.js 24 LTS..."
|
||||
upgrade_node_to_24
|
||||
elif (( major_version > 24 )); then
|
||||
else
|
||||
log_warning "Node.js > 24 detected → script tested only up to Node 24"
|
||||
log "Continuing anyway…"
|
||||
else
|
||||
log_success "Node.js 24 already installed"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -880,22 +908,39 @@ check_node_version() {
|
||||
upgrade_node_to_24() {
|
||||
log "Preparing Node.js 24 upgrade…"
|
||||
|
||||
# Remove old nodesource repo if it exists
|
||||
# Remove old nodesource repo files if they exist
|
||||
if [ -f /etc/apt/sources.list.d/nodesource.list ]; then
|
||||
log "Removing old nodesource.list file..."
|
||||
rm -f /etc/apt/sources.list.d/nodesource.list
|
||||
fi
|
||||
if [ -f /etc/apt/sources.list.d/nodesource.sources ]; then
|
||||
log "Removing old nodesource.sources file..."
|
||||
rm -f /etc/apt/sources.list.d/nodesource.sources
|
||||
fi
|
||||
|
||||
# Update apt cache first
|
||||
log "Updating apt cache..."
|
||||
apt-get update >>"$LOG_FILE" 2>&1 || true
|
||||
|
||||
# Install NodeSource repo for Node.js 24
|
||||
curl -fsSL https://deb.nodesource.com/setup_24.x -o /tmp/node24_setup.sh
|
||||
log "Downloading Node.js 24 setup script..."
|
||||
if ! curl -fsSL https://deb.nodesource.com/setup_24.x -o /tmp/node24_setup.sh; then
|
||||
log_error "Failed to download Node.js 24 setup script"
|
||||
re_enable_service_on_failure
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! bash /tmp/node24_setup.sh >/tmp/node24_setup.log 2>&1; then
|
||||
log_error "Failed to configure Node.js 24 repository"
|
||||
tail -20 /tmp/node24_setup.log | while read -r line; do log_error "$line"; done
|
||||
re_enable_service_on_failure
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Installing Node.js 24…"
|
||||
if ! apt-get install -y nodejs >>"$LOG_FILE" 2>&1; then
|
||||
log_error "Failed to install Node.js 24"
|
||||
re_enable_service_on_failure
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -970,9 +1015,6 @@ main() {
|
||||
# Check Node.js version
|
||||
check_node_version
|
||||
|
||||
#Update Node.js to 24
|
||||
upgrade_node_to_24
|
||||
|
||||
# Download and extract release
|
||||
local source_dir
|
||||
source_dir=$(download_release "$release_info")
|
||||
|
||||
Reference in New Issue
Block a user