This commit is contained in:
2025-07-17 05:51:15 +02:00
parent c3fb1c6cea
commit ebb71deee3
16 changed files with 112 additions and 42 deletions
+2
View File
@@ -39,3 +39,5 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
sentryx/
+22
View File
@@ -23,9 +23,11 @@
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"maplibre-gl": "^5.6.1", "maplibre-gl": "^5.6.1",
"next": "15.4.1", "next": "15.4.1",
"next-themes": "^0.4.6",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1" "tailwind-merge": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {
@@ -3067,6 +3069,16 @@
} }
} }
}, },
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -3439,6 +3451,16 @@
"is-arrayish": "^0.3.1" "is-arrayish": "^0.3.1"
} }
}, },
"node_modules/sonner": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz",
"integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+2
View File
@@ -24,9 +24,11 @@
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"maplibre-gl": "^5.6.1", "maplibre-gl": "^5.6.1",
"next": "15.4.1", "next": "15.4.1",
"next-themes": "^0.4.6",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1" "tailwind-merge": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {
+1 -1
View File
@@ -1 +1 @@
[{"name":"Main Server","ip":"10.7.1.2","coordinates":[19.8187,41.3275]},{"name":"Second Server","ip":"10.7.1.2","coordinates":[19.8187,42]},{"name":"test","ip":"abc","coordinates":[14.212747852350674,13.997221304482736]},{"name":"Bro","ip":"10.7.1.53","coordinates":[19.66455594156352,40.826754398461304]},{"name":"Idk lol","ip":"10.32.2.4","coordinates":[20.135507417925936,41.17975477202245]}] []
+1 -1
View File
@@ -1 +1 @@
{"SESH-344d851e-01ea-4010-8c6a-6205b6772055":"admin","SESH-cc54eaab-a8b9-45d6-8be2-406e346581e8":"admin","SESH-909ab0b4-89e9-4a48-aa16-b8d257421651":"admin","SESH-a930805a-319b-4079-9f81-64a7026e271c":"admin","SESH-8902f651-201d-4493-b1f7-763512cff5b6":"admin"} {"SESH-56474656-1b64-42d7-88e2-e0ed6d1f2084":"admin","SESH-fde4fa01-2f88-4ae4-946e-5c53ffe75a18":"admin","SESH-badb4d6c-e4e9-4f42-b4b9-a46d8de5d227":"admin","SESH-cf69dc29-ad13-42c7-96a1-f6d5d6584b46":"admin","SESH-582e766f-f51b-4c55-bcf6-9aaf9ff859a3":"admin","SESH-029c0d01-9dc1-4efd-bfbe-ee5c645ef38d":"admin"}
+1 -6
View File
@@ -1,6 +1 @@
{ {"admin":{"username":"admin","password":"admin"}}
"admin": {
"username": "admin",
"password": "admin"
}
}
+8 -4
View File
@@ -4,11 +4,11 @@ import Data, { findKey } from "../data";
const users = new Data<{ const users = new Data<{
[key: string]: { username: string; password: string }; [key: string]: { username: string; password: string };
}>("sentryx/users.json", "object"); }>("sentryx/users.json", { admin: { username: "admin", password: "admin" } });
const sessions = new Data<{ [key: string]: string }>( const sessions = new Data<{ [key: string]: string }>(
"sentryx/sessions.json", "sentryx/sessions.json",
"object" {}
); );
export async function POST(request: Request) { export async function POST(request: Request) {
@@ -44,9 +44,9 @@ export async function POST(request: Request) {
export async function GET(request: Request) { export async function GET(request: Request) {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const id = searchParams.get("id") || ""; const sessionId = searchParams.get("sessionId") || "";
if (!sessions.data[id]) if (!sessions.data[sessionId])
return NextResponse.json( return NextResponse.json(
{ message: "Invalid session" }, { message: "Invalid session" },
{ status: HttpStatusCode.Unauthorized } { status: HttpStatusCode.Unauthorized }
@@ -54,3 +54,7 @@ export async function GET(request: Request) {
return NextResponse.json({ message: "ok" }); return NextResponse.json({ message: "ok" });
} }
export default function validate(sessionId: string) {
return sessions.data[sessionId] != undefined;
}
+10 -4
View File
@@ -1,4 +1,5 @@
import { existsSync, readFileSync, writeFileSync } from "fs"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname } from "path";
export function findKey<T extends object>( export function findKey<T extends object>(
obj: T, obj: T,
@@ -12,17 +13,17 @@ export function findKey<T extends object>(
export default class Data<T> { export default class Data<T> {
data: T; data: T;
file_path: string; file_path: string;
constructor(file_path: string, type: "object" | "array") { constructor(file_path: string, def: T) {
this.file_path = file_path; this.file_path = file_path;
if (existsSync(file_path)) { if (existsSync(file_path)) {
try { try {
this.data = JSON.parse(readFileSync(file_path, "utf-8")); this.data = JSON.parse(readFileSync(file_path, "utf-8"));
} catch { } catch {
this.data = (type == "object" ? {} : []) as T; this.data = def;
this.write(); this.write();
} }
} else { } else {
this.data = (type == "object" ? {} : []) as T; this.data = def;
this.write(); this.write();
} }
} }
@@ -32,6 +33,11 @@ export default class Data<T> {
} }
public write() { public write() {
const dir = dirname(this.file_path);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(this.file_path, JSON.stringify(this.data)); writeFileSync(this.file_path, JSON.stringify(this.data));
} }
} }
+9 -2
View File
@@ -2,8 +2,9 @@ import { NextResponse } from "next/server";
import Data from "../data"; import Data from "../data";
import Server, { ServerAPI } from "@/types/server"; import Server, { ServerAPI } from "@/types/server";
import { HttpStatusCode } from "axios"; import { HttpStatusCode } from "axios";
import validate from "../auth/route";
const servers = new Data<ServerAPI[]>("sentryx/servers.json", "array"); const servers = new Data<ServerAPI[]>("sentryx/servers.json", []);
export async function GET(request: Request) { export async function GET(request: Request) {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
@@ -44,10 +45,16 @@ export async function GET(request: Request) {
export async function POST(request: Request) { export async function POST(request: Request) {
const body = await request.json(); const body = await request.json();
if (!validate(body.sessionId))
return NextResponse.json(
{ message: "Invalid session" },
{ status: HttpStatusCode.Unauthorized }
);
if (!body.name || !body.ip) if (!body.name || !body.ip)
return NextResponse.json( return NextResponse.json(
{ {
message: "Invalid body", message: "The name or IP are invalid or empty",
}, },
{ {
status: HttpStatusCode.BadRequest, status: HttpStatusCode.BadRequest,
+17 -2
View File
@@ -8,7 +8,14 @@ import {
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import Server from "@/types/server"; import Server from "@/types/server";
import { Cpu, HardDrive, MemoryStick, Network, Wifi } from "lucide-react"; import {
Cpu,
HardDrive,
MapPin,
MemoryStick,
Network,
Wifi,
} from "lucide-react";
function UsageBar({ function UsageBar({
value, value,
@@ -39,9 +46,17 @@ export default function ServerComponent({ server }: { server: Server }) {
<Card className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer"> <Card className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer">
<CardHeader> <CardHeader>
<CardTitle>{server.name}</CardTitle> <CardTitle>{server.name}</CardTitle>
<CardDescription> <CardDescription className="flex justify-between">
<div className="flex gap-2">
{server.status[0].toUpperCase() + server.status.slice(1)} {" · "} {server.status[0].toUpperCase() + server.status.slice(1)} {" · "}
{server.ip} {server.ip}
{server.location && (
<div className="flex items-center gap-0.5">
<MapPin size={14} />
{server.location}
</div>
)}
</div>
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="w-full flex flex-col gap-3.5"> <CardContent className="w-full flex flex-col gap-3.5">
+2
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import AppLayout from "@/components/AppLayout"; // import your wrapper import AppLayout from "@/components/AppLayout"; // import your wrapper
import { Toaster } from "@/components/ui/sonner";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sentryx Control Panel", title: "Sentryx Control Panel",
@@ -16,6 +17,7 @@ export default function RootLayout({
<html lang="en"> <html lang="en">
<body className="antialiased dark"> <body className="antialiased dark">
<AppLayout>{children}</AppLayout> <AppLayout>{children}</AppLayout>
<Toaster />
</body> </body>
</html> </html>
); );
+26 -3
View File
@@ -15,6 +15,7 @@ import { Label } from "@/components/ui/label";
import MapSelect from "./LocationSelect"; import MapSelect from "./LocationSelect";
import { useState } from "react"; import { useState } from "react";
import Session from "@/lib/session"; import Session from "@/lib/session";
import { toast } from "sonner";
export default function NewServerDialog({ export default function NewServerDialog({
session, session,
@@ -24,7 +25,8 @@ export default function NewServerDialog({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [ip, setIp] = useState(""); const [ip, setIp] = useState("");
const [location, setLocation] = useState<[number, number] | undefined>( const [location, setLocation] = useState<string | undefined>(undefined);
const [coordinates, setCoordinates] = useState<[number, number] | undefined>(
undefined undefined
); );
@@ -52,12 +54,33 @@ export default function NewServerDialog({
onInput={(e) => setIp(e.currentTarget.value)} onInput={(e) => setIp(e.currentTarget.value)}
/> />
<Label className="mt-2">Server Location Name</Label>
<Input
placeholder="Home, Warehouse 1, 4th ave, etc"
onInput={(e) =>
setLocation(
e.currentTarget.value == "" ? undefined : e.currentTarget.value
)
}
/>
<Label className="mt-2">Server Location</Label> <Label className="mt-2">Server Location</Label>
<MapSelect setLocation={setLocation} /> <MapSelect setLocation={setCoordinates} />
<DialogFooter> <DialogFooter>
<Button <Button
onClick={() => { onClick={() => {
session?.addServer({ name, ip, coordinates: location }); session
?.addServer({ name, ip, coordinates, location })
.then(() => {
toast.success(
"Server added successfully, refresh to view it"
);
})
.catch((e) => {
toast.error(
"Error adding server: " + e.response.data.message
);
});
setOpen(false); setOpen(false);
}} }}
> >
+1 -1
View File
@@ -14,7 +14,7 @@ export default async function getSession(): Promise<Session | null> {
try { try {
await axios.get("http://localhost:3000/api/auth", { await axios.get("http://localhost:3000/api/auth", {
params: { id: sessionId }, params: { sessionId },
}); });
return new Session(sessionId); return new Session(sessionId);
+5 -8
View File
@@ -9,16 +9,13 @@ export default function useSession() {
const [session, setSession] = useState<Session | null>(null); const [session, setSession] = useState<Session | null>(null);
useEffect(() => { useEffect(() => {
const id = Cookies.get("session_id"); const sessionId = Cookies.get("session_id");
if (id) if (sessionId)
axios({ axios
method: "get", .get("/api/auth", { params: { sessionId } })
url: "/api/auth",
params: { id },
})
.then((r) => { .then((r) => {
setSession(new Session(id)); setSession(new Session(sessionId));
}) })
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
+2 -8
View File
@@ -13,7 +13,7 @@ export default class Session {
return ( return (
(( ((
await axios.get(`/api/servers?s=${query || ""}`, { await axios.get(`/api/servers?s=${query || ""}`, {
params: { id: this.id }, params: { sessionId: this.id },
}) })
).data.servers as Server[]) || [] ).data.servers as Server[]) || []
); );
@@ -23,12 +23,6 @@ export default class Session {
} }
public async addServer(s: ServerAPI) { public async addServer(s: ServerAPI) {
try { return await axios.post("/api/servers", { ...s, sessionId: this.id });
return (
((await axios.post("/api/servers", s)).data.servers as Server[]) || []
);
} catch {
return [];
}
} }
} }
+1
View File
@@ -1,6 +1,7 @@
export interface ServerAPI { export interface ServerAPI {
name: string; name: string;
ip: string; ip: string;
location?: string;
coordinates?: [number, number]; coordinates?: [number, number];
} }