diff --git a/frontend/src/components/admin/servers/RestartServer.tsx b/frontend/src/components/admin/servers/RestartServer.tsx index 87bded7..3d7d20b 100644 --- a/frontend/src/components/admin/servers/RestartServer.tsx +++ b/frontend/src/components/admin/servers/RestartServer.tsx @@ -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 (
- -

Restart Server ... Needs added still

+

+ Restart Server, note you might see the screen error + out for a second +

diff --git a/frontend/src/components/admin/servers/ServerPage.tsx b/frontend/src/components/admin/servers/ServerPage.tsx index e9203a9..81132e7 100644 --- a/frontend/src/components/admin/servers/ServerPage.tsx +++ b/frontend/src/components/admin/servers/ServerPage.tsx @@ -171,11 +171,17 @@ export default function ServerPage() { server={server} token={token as string} /> - + + - + +
)} diff --git a/frontend/src/components/admin/servers/StartServer.tsx b/frontend/src/components/admin/servers/StartServer.tsx index 99a7fac..dbe995e 100644 --- a/frontend/src/components/admin/servers/StartServer.tsx +++ b/frontend/src/components/admin/servers/StartServer.tsx @@ -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 (
- -

Start Server ... Needs added still

+

Start Server

diff --git a/frontend/src/components/admin/servers/StopServer.tsx b/frontend/src/components/admin/servers/StopServer.tsx index d9b6219..7b0017c 100644 --- a/frontend/src/components/admin/servers/StopServer.tsx +++ b/frontend/src/components/admin/servers/StopServer.tsx @@ -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) { -

Stop Server ... Needs added still

+

Stop Server

diff --git a/server/globalUtils/rateLimiter.ts b/server/globalUtils/rateLimiter.ts new file mode 100644 index 0000000..cd4349f --- /dev/null +++ b/server/globalUtils/rateLimiter.ts @@ -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(); + +// --- 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; diff --git a/server/scripts/services.ps1 b/server/scripts/services.ps1 index 9b33606..dc4c030 100644 --- a/server/scripts/services.ps1 +++ b/server/scripts/services.ps1 @@ -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 + } } } + + diff --git a/server/services/server/controller/server/serviceControl.ts b/server/services/server/controller/server/serviceControl.ts index 9487b73..b0542c8 100644 --- a/server/services/server/controller/server/serviceControl.ts +++ b/server/services/server/controller/server/serviceControl.ts @@ -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); diff --git a/server/services/server/route/servers/serverContorl.ts b/server/services/server/route/servers/serverContorl.ts index fac342d..4cc9cf6 100644 --- a/server/services/server/route/servers/serverContorl.ts +++ b/server/services/server/route/servers/serverContorl.ts @@ -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,