feat(server service): added in start stop and restart from vms036

This commit is contained in:
2025-06-09 11:42:27 -05:00
parent a4dab55f7a
commit 29e0347f84
8 changed files with 400 additions and 110 deletions

View File

@@ -1,19 +1,72 @@
import {Button} from "@/components/ui/button";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
import {RotateCcw} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import axios from "axios";
import { RotateCcw } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
export default function RestartServer(data: any) {
const token = localStorage.getItem("auth_token");
const [disable, setDisable] = useState(false);
const handleRestartServer = async (plant: string) => {
toast.success(`${plant} is being restarted please wait.`);
setDisable(true);
let data: any = {
processType: "restart",
plantToken: plant,
};
const url: string = window.location.host.split(":")[0];
if (url === "localhost" || url === "usmcd1vms036") {
data = { ...data, remote: "true" };
}
//console.log(data);
try {
const res = await axios.post("/api/server/serviceprocess", data, {
headers: { Authorization: `Bearer ${token}` },
});
//console.log(res);
if (res.status === 200) {
setTimeout(() => {
toast.success(`${plant} Has beed restarted.`);
setDisable(false);
}, 3000);
}
} catch (error) {
console.log(error);
}
};
export default function RestartServer() {
return (
<div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant={"outline"} size={"icon"}>
<Button
variant={"outline"}
size={"icon"}
disabled={disable}
onClick={() =>
handleRestartServer(data.plantData.plantToken)
}
>
<RotateCcw />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Restart Server ... Needs added still</p>
<p>
Restart Server, note you might see the screen error
out for a second
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -171,11 +171,17 @@ export default function ServerPage() {
server={server}
token={token as string}
/>
<StartServer />
<StartServer
plantData={server}
/>
<StopServer
plantData={server}
/>
<RestartServer />
<RestartServer
plantData={server}
/>
</div>
)}
</TableCell>

View File

@@ -1,19 +1,71 @@
import {Button} from "@/components/ui/button";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
import {Play} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import axios from "axios";
import { Play } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
export default function StartServer() {
export default function StartServer(data: any) {
const token = localStorage.getItem("auth_token");
const [disable, setDisable] = useState(false);
const handleStartServer = async (plant: string) => {
toast.success(`${plant} is being started please wait.`);
setDisable(true);
let data: any = {
processType: "start",
plantToken: plant,
};
const url: string = window.location.host.split(":")[0];
if (url === "localhost" || url === "usmcd1vms036") {
data = { ...data, remote: "true" };
}
//console.log(data);
try {
const res = await axios.post("/api/server/serviceprocess", data, {
headers: { Authorization: `Bearer ${token}` },
});
//console.log(res);
if (res.status === 200) {
setTimeout(() => {
toast.success(`${plant} Has beed started.`);
setDisable(false);
}, 3000);
}
} catch (error: any) {
if (error.status === 429) {
toast.error(error.response.data.message);
setDisable(false);
}
}
};
return (
<div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant={"outline"} size={"icon"}>
<Button
variant={"outline"}
size={"icon"}
disabled={disable}
onClick={() =>
handleStartServer(data.plantData.plantToken)
}
>
<Play />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Start Server ... Needs added still</p>
<p>Start Server</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -7,24 +7,40 @@ import {
} from "@/components/ui/tooltip";
import axios from "axios";
import { Octagon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
export default function StopServer(plantData: any) {
export default function StopServer(data: any) {
const token = localStorage.getItem("auth_token");
const [disable, setDisable] = useState(false);
const handleStopServer = async (plant: string) => {
toast.success(`${plant} is being stopped please wait.`);
setDisable(true);
let data: any = {
processType: "stop",
plantToken: plant,
};
const url: string = window.location.host.split(":")[0];
if (url === "localhost") {
if (url === "localhost" || url === "usmcd1vms036") {
data = { ...data, remote: "true" };
}
//console.log(data);
try {
const res = await axios.post("/api/server/serviceprocess", data, {
headers: { Authorization: `Bearer ${token}` },
});
console.log(res);
//console.log(res);
if (res.status === 200) {
setTimeout(() => {
toast.success(`${plant} Has beed stopped.`);
setDisable(false);
}, 3000);
}
} catch (error) {
console.log(error);
}
@@ -37,15 +53,16 @@ export default function StopServer(plantData: any) {
<Button
variant="destructive"
size={"icon"}
disabled={disable}
onClick={() =>
handleStopServer(plantData.plantToken)
handleStopServer(data.plantData.plantToken)
}
>
<Octagon />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Stop Server ... Needs added still</p>
<p>Stop Server</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -0,0 +1,77 @@
import { Hono } from "hono";
import { type Context, type Next } from "hono";
const app = new Hono();
// --- In-Memory Store for Rate Limits ---
// This Map will store when each user/key last accessed a rate-limited endpoint.
// Key: string (e.g., 'ip_address' or 'user_id_endpoint')
// Value: number (timestamp of last access in milliseconds)
const rateLimitStore = new Map<string, number>();
// --- Configuration ---
const FIFTEEN_MINUTES_MS = 5 * 60 * 1000; // 15 minutes in milliseconds
// --- Rate Limiting Middleware ---
export const simpleRateLimit = async (c: Context, next: Next) => {
// 1. Define a unique key for the rate limit
// For simplicity, we'll use a placeholder for user identification.
// In a real app:
// - If unauthenticated: Use c.req.header('x-forwarded-for') or c.req.ip (if configured/available)
// - If authenticated: Get user ID from c.req.user or similar after authentication middleware
const userIdentifier = c.req.header("x-forwarded-for") || "anonymous_user"; // Basic IP-like identifier
// You can also make the key specific to the route to have different limits per route
const routeKey = `${userIdentifier}:${c.req.path}`;
const now = Date.now();
const lastAccessTime = rateLimitStore.get(routeKey);
if (lastAccessTime) {
const timeElapsed = now - lastAccessTime;
if (timeElapsed < FIFTEEN_MINUTES_MS) {
// Limit exceeded
const timeRemainingMs = FIFTEEN_MINUTES_MS - timeElapsed;
const timeRemainingSeconds = Math.ceil(timeRemainingMs / 1000);
c.status(429); // HTTP 429: Too Many Requests
return c.json({
error: "Too Many Requests",
message: `Please wait ${timeRemainingSeconds} seconds before trying again.`,
retryAfter: timeRemainingSeconds, // Standard header for rate limiting clients
});
}
}
// If no previous access, or the 15 minutes have passed, allow the request
// and update the last access time.
rateLimitStore.set(routeKey, now);
// Continue to the next middleware or route handler
await next();
};
// --- Apply the Middleware to Specific Routes ---
app.get("/", (c) => {
return c.text("Welcome! This is a public endpoint.");
});
// This endpoint will be rate-limited
app.get("/privileged", simpleRateLimit, (c) => {
return c.text("You successfully accessed the privileged endpoint!");
});
// Another rate-limited endpoint
app.post("/submit-data", simpleRateLimit, async (c) => {
// In a real app, you'd process form data or JSON here
return c.text("Data submitted successfully (rate-limited).");
});
// Example of an endpoint that is NOT rate-limited
app.get("/health", (c) => {
return c.text("Server is healthy!");
});
export default app;

View File

@@ -4,7 +4,10 @@ param (
[string]$appPath,
[string]$command, # just the command like run start or what ever you have in npm.
[string]$description,
[string]$remote
[string]$remote,
[string]$server,
[string]$username,
[string]$admpass
)
# Example string to run with the parameters in it.
@@ -15,108 +18,174 @@ param (
$nssmPath = $AppPath + "\nssm.exe"
$npmPath = "C:\Program Files\nodejs\npm.cmd" # Path to npm.cmd
# Convert the plain-text password to a SecureString
$securePass = ConvertTo-SecureString $admpass -AsPlainText -Force
$credentials = New-Object System.Management.Automation.PSCredential($username, $securePass)
if($remote -eq "true"){
# if(-not $username -or -not $admpass){
# Write-host "Missing adm account info please try again."
# exit 1
# }
$plantFunness = {
param ($service, $processType, $location)
# Call your PowerShell script inside plantFunness
& "$($location)\dist\server\scripts\services.ps1" -serviceName $service -option $processType -appPath $location
# & "$($location)\dist\server\scripts\services.ps1" -serviceName $service -option $processType -appPath $location
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Host "Error: This script must be run as Administrator."
exit 1
}
if(-not $service -or -not $processType){
Write-host "The service name or option is missing please enter one of them and try again."
exit 1
}
if ($processType -eq "start"){
write-host "Starting $($service)."
Start-Service $service
}
if ($processType -eq "stop"){
write-host "Stoping $($service)."
Stop-Service $service
}
if ($processType -eq "restart"){
write-host "Stoping $($service) to be restarted"
Stop-Service $service
Start-Sleep 3 # so we give it enough time to fully stop
write-host "Starting $($service)"
Start-Service $service
}
if ($processType -eq "prodStop"){
if(-not $location){
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
& $nssmPath stop $service
write-host "Removing $($service)"
#& $nssmPath remove $serviceName confirm
sc.exe config $service start= disabled
}
if ($processType -eq "prodStart"){
if(-not $location){
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
& $nssmPath start $service
write-host "Removing $($service)"
#& $nssmPath remove $serviceName confirm
sc.exe config $service start= auto
}
}
Invoke-Command -ComputerName $server -ScriptBlock $plantFunness -ArgumentList $service, $option, $appPath -Credential $credentials
}
Invoke-Command -ComputerName $server -ScriptBlock $plantFunness -ArgumentList $serviceName, $option, $appPath -Credential $credentials
} else {
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Host "Error: This script must be run as Administrator."
exit 1
}
if(-not $serviceName -or -not $option){
Write-host "The service name or option is missing please enter one of them and try again."
exit 1
}
if ($option -eq "start"){
write-host "Starting $($serviceName)."
Start-Service $serviceName
}
if ($option -eq "stop"){
write-host "Stoping $($serviceName)."
Stop-Service $serviceName
}
if ($option -eq "restart"){
write-host "Stoping $($serviceName) to be restarted"
Stop-Service $serviceName
Start-Sleep 3 # so we give it enough time to fully stop
write-host "Starting $($serviceName)"
Start-Service $serviceName
}
if ($option -eq "delete"){
if(-not $appPath){
Write-host "The path to the app is missing please add it in and try again."
exit 1
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Host "Error: This script must be run as Administrator."
exit 1
}
& $nssmPath stop $serviceName
write-host "Removing $($serviceName)"
& $nssmPath remove $serviceName confirm
}
if ($option -eq "prodStop"){
if(-not $appPath){
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
& $nssmPath stop $serviceName
write-host "Removing $($serviceName)"
#& $nssmPath remove $serviceName confirm
sc.exe config $serviceName start= disabled
}
if ($option -eq "prodStart"){
if(-not $appPath){
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
& $nssmPath start $serviceName
write-host "Removing $($serviceName)"
#& $nssmPath remove $serviceName confirm
sc.exe config $serviceName start= auto
}
if($option -eq "install"){
if(-not $appPath -or -not $description -or -not $command){
Write-host "Please check all parameters are passed to install the app.."
if(-not $serviceName -or -not $option){
Write-host "The service name or option is missing please enter one of them and try again."
exit 1
}
if ($option -eq "start"){
write-host "Starting $($serviceName)."
Start-Service $serviceName
}
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if(-not $service){
write-host $serviceName "is not installed we will install it now"
if ($option -eq "stop"){
write-host "Stoping $($serviceName)."
Stop-Service $serviceName
}
Write-Host "Installing $serviceName..."
& $nssmPath install $serviceName $npmPath $command
& $nssmPath set $serviceName AppDirectory $appPath
& $nssmPath set $serviceName Description $description
# Set recovery options
sc.exe failure $serviceName reset= 0 actions= restart/5000/restart/5000/restart/5000
& $nssmPath start $serviceName
}else{
write-host $serviceName "is already installed will push the updated info"
Write-Host "Updating $serviceName..."
if ($option -eq "restart"){
write-host "Stoping $($serviceName) to be restarted"
Stop-Service $serviceName
Start-Sleep 3 # so we give it enough time to fully stop
write-host "Starting $($serviceName)"
Start-Service $serviceName
}
if ($option -eq "delete"){
if(-not $appPath){
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
& $nssmPath stop $serviceName
& $nssmPath set $serviceName AppDirectory $appPath
& $nssmPath set $serviceName Description $description
# Set recovery options
sc.exe failure $serviceName reset= 0 actions= restart/5000/restart/5000/restart/5000
Start-Sleep 4
write-host "Removing $($serviceName)"
& $nssmPath remove $serviceName confirm
}
if ($option -eq "prodStop"){
if(-not $appPath){
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
& $nssmPath stop $serviceName
write-host "Removing $($serviceName)"
#& $nssmPath remove $serviceName confirm
sc.exe config $serviceName start= disabled
}
if ($option -eq "prodStart"){
if(-not $appPath){
Write-host "The path to the app is missing please add it in and try again."
exit 1
}
& $nssmPath start $serviceName
write-host "Removing $($serviceName)"
#& $nssmPath remove $serviceName confirm
sc.exe config $serviceName start= auto
}
if($option -eq "install"){
if(-not $appPath -or -not $description -or -not $command){
Write-host "Please check all parameters are passed to install the app.."
exit 1
}
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if(-not $service){
write-host $serviceName "is not installed we will install it now"
Write-Host "Installing $serviceName..."
& $nssmPath install $serviceName $npmPath $command
& $nssmPath set $serviceName AppDirectory $appPath
& $nssmPath set $serviceName Description $description
# Set recovery options
sc.exe failure $serviceName reset= 0 actions= restart/5000/restart/5000/restart/5000
& $nssmPath start $serviceName
}else{
write-host $serviceName "is already installed will push the updated info"
Write-Host "Updating $serviceName..."
& $nssmPath stop $serviceName
& $nssmPath set $serviceName AppDirectory $appPath
& $nssmPath set $serviceName Description $description
# Set recovery options
sc.exe failure $serviceName reset= 0 actions= restart/5000/restart/5000/restart/5000
Start-Sleep 4
& $nssmPath start $serviceName
}
}
}

View File

@@ -2,7 +2,6 @@ import { spawn } from "child_process";
import { eq } from "drizzle-orm";
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
import { db } from "../../../../../database/dbclient.js";
import { settings } from "../../../../../database/schema/settings.js";
import { createLog } from "../../../logger/logger.js";
import { serverData } from "../../../../../database/schema/serverData.js";
import os from "os";
@@ -33,6 +32,12 @@ export const serviceControl = async (
scriptPath = `${process.env.DEVFOLDER}\\dist\\server\\scripts\\services.ps1`;
}
console.log(serverInfo[0].serverDNS);
const username = process.env.ADMUSER as string;
const password = process.env.ADMPASSWORD as string;
console.log(username, password);
const args = [
"-NoProfile",
"-ExecutionPolicy",
@@ -47,6 +52,12 @@ export const serviceControl = async (
serverInfo[0].serverLoc as string,
"-remote",
remote ?? "",
"-server",
serverInfo[0].serverDNS as string,
"-username",
username,
"-admpass",
password,
];
const scriptProcess = spawn("powershell", args);

View File

@@ -5,6 +5,7 @@ import { tryCatch } from "../../../../globalUtils/tryCatch.js";
import hasCorrectRole from "../../../auth/middleware/roleCheck.js";
import { serviceControl } from "../../controller/server/serviceControl.js";
import { apiHit } from "../../../../globalUtils/apiHits.js";
import { simpleRateLimit } from "../../../../globalUtils/rateLimiter.js";
// Define the request body schema
const requestSchema = z.object({
@@ -21,7 +22,11 @@ app.openapi(
summary: "Starts, Stops, Restarts the server.",
method: "post",
path: "/serviceprocess",
middleware: [authMiddleware, hasCorrectRole(["systemAdmin"], "admin")],
middleware: [
authMiddleware,
hasCorrectRole(["systemAdmin"], "admin"),
simpleRateLimit,
],
request: {
body: {
@@ -34,7 +39,7 @@ app.openapi(
}),
async (c) => {
const { data, error } = await tryCatch(c.req.json());
//apiHit(c, { endpoint: `/serviceprocess`, lastBody: data });
//apiHit(c, { endpoint: "/bookin", lastBody: data });
if (error) {
return c.json({
success: false,