Revert "cleanup v2"

This reverts commit 8b82578809.
This commit is contained in:
headlesdev
2025-05-17 12:31:27 +02:00
parent 8b82578809
commit 94ba5e20c6
318 changed files with 54608 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface AddRequest {
serverId: number;
name: string;
description: string;
icon: string;
publicURL: string;
localURL: string;
uptimecheckUrl: string;
}
export async function POST(request: NextRequest) {
try {
const body: AddRequest = await request.json();
const { serverId, name, description, icon, publicURL, localURL, uptimecheckUrl } = body;
const application = await prisma.application.create({
data: {
serverId,
name,
description,
icon,
publicURL,
localURL,
uptimecheckUrl
}
});
return NextResponse.json({ message: "Success", application });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,25 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const id = Number(body.id);
if (!id) {
return NextResponse.json({ error: "Missing ID" }, { status: 400 });
}
await prisma.application.delete({
where: { id: id }
});
await prisma.uptime_history.deleteMany({
where: { applicationId: id }
});
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,42 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface EditRequest {
id: number;
name: string;
description: string;
serverId: number;
icon: string;
publicURL: string;
localURL: string;
uptimecheckUrl: string;
}
export async function PUT(request: NextRequest) {
try {
const body: EditRequest = await request.json();
const { id, name, description, serverId, icon, publicURL, localURL, uptimecheckUrl } = body;
const existingApp = await prisma.application.findUnique({ where: { id } });
if (!existingApp) {
return NextResponse.json({ error: "Server not found" }, { status: 404 });
}
const updatedApplication = await prisma.application.update({
where: { id },
data: {
serverId,
name,
description,
icon,
publicURL,
localURL,
uptimecheckUrl
}
});
return NextResponse.json({ message: "Application updated", application: updatedApplication });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,50 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface PostRequest {
page?: number;
ITEMS_PER_PAGE?: number;
}
export async function POST(request: NextRequest) {
try {
const body: PostRequest = await request.json();
const page = Math.max(1, body.page || 1);
const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 10;
const [applications, totalCount, servers_all] = await Promise.all([
prisma.application.findMany({
skip: (page - 1) * ITEMS_PER_PAGE,
take: ITEMS_PER_PAGE,
orderBy: { name: "asc" }
}),
prisma.application.count(),
prisma.server.findMany()
]);
const serverIds = applications
.map((app: { serverId: number | null }) => app.serverId)
.filter((id:any): id is number => id !== null);
const servers = await prisma.server.findMany({
where: { id: { in: serverIds } }
});
const applicationsWithServers = applications.map((app: any) => ({
...app,
server: servers.find((s: any) => s.id === app.serverId)?.name || "No server"
}));
const maxPage = Math.ceil(totalCount / ITEMS_PER_PAGE);
return NextResponse.json({
applications: applicationsWithServers,
servers: servers_all,
maxPage,
totalItems: totalCount
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 });
}
}

View File

@@ -0,0 +1,48 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import Fuse from "fuse.js";
interface SearchRequest {
searchterm: string;
}
export async function POST(request: NextRequest) {
try {
const body: SearchRequest = await request.json();
const { searchterm } = body;
const applications = await prisma.application.findMany({});
const fuseOptions = {
keys: ['name', 'description'],
threshold: 0.3,
includeScore: true,
};
const fuse = new Fuse(applications, fuseOptions);
const searchResults = fuse.search(searchterm);
const searchedApps = searchResults.map(({ item }) => item);
// Get server IDs from the search results
const serverIds = searchedApps
.map(app => app.serverId)
.filter((id): id is number => id !== null);
// Fetch server data for these applications
const servers = await prisma.server.findMany({
where: { id: { in: serverIds } }
});
// Add server name to each application
const results = searchedApps.map(app => ({
...app,
server: servers.find(s => s.id === app.serverId)?.name || "No server"
}));
return NextResponse.json({ results });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,175 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface RequestBody {
timespan?: number;
page?: number;
itemsPerPage?: number;
}
const getTimeRange = (timespan: number) => {
const now = new Date();
switch (timespan) {
case 1:
return {
start: new Date(now.getTime() - 60 * 60 * 1000),
interval: 'minute'
};
case 2:
return {
start: new Date(now.getTime() - 24 * 60 * 60 * 1000),
interval: 'hour'
};
case 3:
return {
start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
interval: 'day'
};
case 4:
return {
start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
interval: 'day'
};
default:
return {
start: new Date(now.getTime() - 60 * 60 * 1000),
interval: 'minute'
};
}
};
const generateIntervals = (timespan: number) => {
const now = new Date();
now.setSeconds(0, 0);
switch (timespan) {
case 1: // 1 hour - 60 one-minute intervals
return Array.from({ length: 60 }, (_, i) => {
const d = new Date(now);
d.setMinutes(d.getMinutes() - i);
d.setSeconds(0, 0);
return d;
});
case 2: // 1 day - 24 one-hour intervals
return Array.from({ length: 24 }, (_, i) => {
const d = new Date(now);
d.setHours(d.getHours() - i);
d.setMinutes(0, 0, 0);
return d;
});
case 3: // 7 days
return Array.from({ length: 7 }, (_, i) => {
const d = new Date(now);
d.setDate(d.getDate() - i);
d.setHours(0, 0, 0, 0);
return d;
});
case 4: // 30 days
return Array.from({ length: 30 }, (_, i) => {
const d = new Date(now);
d.setDate(d.getDate() - i);
d.setHours(0, 0, 0, 0);
return d;
});
default:
return [];
}
};
const getIntervalKey = (date: Date, timespan: number) => {
const d = new Date(date);
switch (timespan) {
case 1: // 1 hour - minute intervals
d.setSeconds(0, 0);
return d.toISOString();
case 2: // 1 day - hour intervals
d.setMinutes(0, 0, 0);
return d.toISOString();
case 3: // 7 days - day intervals
case 4: // 30 days - day intervals
d.setHours(0, 0, 0, 0);
return d.toISOString();
default:
return d.toISOString();
}
};
export async function POST(request: NextRequest) {
try {
const { timespan = 1, page = 1, itemsPerPage = 5 }: RequestBody = await request.json();
const skip = (page - 1) * itemsPerPage;
// Get paginated and sorted applications
const [applications, totalCount] = await Promise.all([
prisma.application.findMany({
skip,
take: itemsPerPage,
orderBy: { name: 'asc' }
}),
prisma.application.count()
]);
const applicationIds = applications.map(app => app.id);
// Get time range and intervals
const { start } = getTimeRange(timespan);
const intervals = generateIntervals(timespan);
// Get uptime history for the filtered applications
const uptimeHistory = await prisma.uptime_history.findMany({
where: {
applicationId: { in: applicationIds },
createdAt: { gte: start }
},
orderBy: { createdAt: "desc" }
});
// Process data for each application
const result = applications.map(app => {
const appChecks = uptimeHistory.filter(check => check.applicationId === app.id);
const checksMap = new Map<string, { failed: number; total: number }>();
for (const check of appChecks) {
const intervalKey = getIntervalKey(check.createdAt, timespan);
const current = checksMap.get(intervalKey) || { failed: 0, total: 0 };
current.total++;
if (!check.online) current.failed++;
checksMap.set(intervalKey, current);
}
const uptimeSummary = intervals.map(interval => {
const intervalKey = getIntervalKey(interval, timespan);
const stats = checksMap.get(intervalKey);
return {
timestamp: intervalKey,
missing: !stats,
online: stats ? (stats.failed / stats.total) <= 0.5 : null
};
});
return {
appName: app.name,
appId: app.id,
uptimeSummary
};
});
return NextResponse.json({
data: result,
pagination: {
currentPage: page,
totalPages: Math.ceil(totalCount / itemsPerPage),
totalItems: totalCount
}
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 });
}
}

View File

@@ -0,0 +1,56 @@
import { NextResponse, NextRequest } from "next/server";
import jwt from 'jsonwebtoken';
import { prisma } from "@/lib/prisma";
interface EditEmailRequest {
newEmail: string;
jwtToken: string;
}
export async function POST(request: NextRequest) {
try {
const body: EditEmailRequest = await request.json();
const { newEmail, jwtToken } = body;
// Ensure JWT_SECRET is defined
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET is not defined');
}
// Verify JWT
const decoded = jwt.verify(jwtToken, process.env.JWT_SECRET) as { account_secret: string };
if (!decoded.account_secret) {
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
}
// Get the user by account id
const user = await prisma.user.findUnique({
where: { id: decoded.account_secret },
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Check if the new email is already in use
const existingUser = await prisma.user.findUnique({
where: { email: newEmail },
});
if (existingUser) {
return NextResponse.json({ error: 'Email already in use' }, { status: 400 });
}
// Update the user's email
await prisma.user.update({
where: { id: user.id },
data: { email: newEmail },
});
return NextResponse.json({ message: 'Email updated successfully' });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,55 @@
import { NextResponse, NextRequest } from "next/server";
import jwt from 'jsonwebtoken';
import { prisma } from "@/lib/prisma";
import bcrypt from 'bcryptjs';
interface EditEmailRequest {
oldPassword: string;
newPassword: string;
jwtToken: string;
}
export async function POST(request: NextRequest) {
try {
const body: EditEmailRequest = await request.json();
const { oldPassword, newPassword, jwtToken } = body;
// Ensure JWT_SECRET is defined
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET is not defined');
}
// Verify JWT
const decoded = jwt.verify(jwtToken, process.env.JWT_SECRET) as { account_secret: string };
if (!decoded.account_secret) {
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
}
// Get the user by account id
const user = await prisma.user.findUnique({
where: { id: decoded.account_secret },
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Check if the old password is correct
const isOldPasswordValid = await bcrypt.compare(oldPassword, user.password);
if (!isOldPasswordValid) {
return NextResponse.json({ error: 'Old password is incorrect' }, { status: 401 });
}
// Hash the new password
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
// Update the user's password
await prisma.user.update({
where: { id: user.id },
data: { password: hashedNewPassword },
});
return NextResponse.json({ message: 'Password updated successfully' });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,65 @@
import { NextResponse, NextRequest } from "next/server";
import jwt from 'jsonwebtoken';
import { prisma } from "@/lib/prisma";
import bcrypt from 'bcryptjs';
interface LoginRequest {
username: string;
password: string;
}
export async function POST(request: NextRequest) {
try {
const body: LoginRequest = await request.json();
const { username, password } = body;
// Ensure JWT_SECRET is defined
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET is not defined');
}
let accountId: string = '';
// Check if there are any entries in user
const userCount = await prisma.user.count();
if (userCount === 0) {
if(username=== "admin@example.com" && password === "admin") {
// Hash the password
const hashedPassword = await bcrypt.hash(password, 10);
// Create the first user with hashed password
const user = await prisma.user.create({
data: {
email: username,
password: hashedPassword,
},
});
// Get the account id
accountId = user.id;
} else {
return NextResponse.json({ error: "Wrong credentials" }, { status: 401 });
}
} else {
// Get the user by username
const user = await prisma.user.findUnique({
where: { email: username },
});
if (!user) {
return NextResponse.json({ error: "Wrong credentials" }, { status: 401 });
}
// Check if the password is correct
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return NextResponse.json({ error: "Wrong credentials" }, { status: 401 });
}
// Get the account id
accountId = user.id;
}
// Create JWT
const token = jwt.sign({ account_secret: accountId }, process.env.JWT_SECRET, { expiresIn: '7d' });
return NextResponse.json({ token });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { prisma } from "@/lib/prisma";
interface ValidateRequest {
token: string;
}
export async function POST(request: NextRequest) {
try {
const body: ValidateRequest = await request.json();
const { token } = body;
// Ensure JWT_SECRET is defined
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET is not defined');
}
// Get the account id
const user = await prisma.user.findFirst({
where: {},
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Verify JWT
const decoded = jwt.verify(token, process.env.JWT_SECRET) as JwtPayload & { id: string };
if(!decoded.account_secret) {
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
}
if(decoded.account_secret !== user.id) {
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
}
return NextResponse.json({ message: 'Valid token' });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,35 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) {
try {
const serverCountNoVMs = await prisma.server.count({
where: {
hostServer: 0
}
});
const serverCountOnlyVMs = await prisma.server.count({
where: {
hostServer: {
not: 0
}
}
});
const applicationCount = await prisma.application.count();
const onlineApplicationsCount = await prisma.application.count({
where: { online: true }
});
return NextResponse.json({
serverCountNoVMs,
serverCountOnlyVMs,
applicationCount,
onlineApplicationsCount
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

409
app/api/flowchart/route.ts Normal file
View File

@@ -0,0 +1,409 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface Node {
id: string;
type: string;
data: {
label: string;
[key: string]: any;
};
position: { x: number; y: number };
style: React.CSSProperties;
draggable?: boolean;
selectable?: boolean;
zIndex?: number;
}
interface Edge {
id: string;
source: string;
target: string;
type: string;
style: {
stroke: string;
strokeWidth: number;
};
}
interface Server {
id: number;
name: string;
ip: string;
host: boolean;
hostServer: number | null;
}
interface Application {
id: number;
name: string;
localURL: string;
serverId: number;
}
const NODE_WIDTH = 220;
const NODE_HEIGHT = 60;
const APP_NODE_WIDTH = 160;
const APP_NODE_HEIGHT = 40;
const HORIZONTAL_SPACING = 700;
const VERTICAL_SPACING = 80;
const START_Y = 120;
const ROOT_NODE_WIDTH = 300;
const CONTAINER_PADDING = 40;
const COLUMN_SPACING = 220;
const VM_APP_SPACING = 220;
const MIN_VM_SPACING = 10;
const APP_ROW_SPACING = 15;
export async function GET() {
try {
const [servers, applications] = await Promise.all([
prisma.server.findMany({
orderBy: { id: "asc" },
}) as Promise<Server[]>,
prisma.application.findMany({
orderBy: { serverId: "asc" },
}) as Promise<Application[]>,
]);
// Level 2: Physical Servers
const serverNodes: Node[] = servers
.filter(server => !server.hostServer)
.map((server, index, filteredServers) => {
const xPos =
index * HORIZONTAL_SPACING -
((filteredServers.length - 1) * HORIZONTAL_SPACING) / 2;
return {
id: `server-${server.id}`,
type: "server",
data: {
label: `${server.name}\n${server.ip}`,
...server,
},
position: { x: xPos, y: START_Y },
style: {
background: "#ffffff",
color: "#0f0f0f",
border: "2px solid #e6e4e1",
borderRadius: "4px",
padding: "8px",
width: NODE_WIDTH,
height: NODE_HEIGHT,
fontSize: "0.9rem",
lineHeight: "1.2",
whiteSpace: "pre-wrap",
},
};
});
// Level 3: Services and VMs
const serviceNodes: Node[] = [];
const vmNodes: Node[] = [];
servers.forEach((server) => {
const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`);
if (serverNode) {
const serverX = serverNode.position.x;
// Services (left column)
applications
.filter(app => app.serverId === server.id)
.forEach((app, appIndex) => {
serviceNodes.push({
id: `service-${app.id}`,
type: "service",
data: {
label: `${app.name}\n${app.localURL}`,
...app,
},
position: {
x: serverX - COLUMN_SPACING,
y: START_Y + NODE_HEIGHT + VERTICAL_SPACING + appIndex * (APP_NODE_HEIGHT + 20),
},
style: {
background: "#f0f9ff",
color: "#0f0f0f",
border: "2px solid #60a5fa",
borderRadius: "4px",
padding: "6px",
width: APP_NODE_WIDTH,
height: APP_NODE_HEIGHT,
fontSize: "0.8rem",
lineHeight: "1.1",
whiteSpace: "pre-wrap",
},
});
});
// VMs (middle column) mit dynamischem Abstand
const hostVMs = servers.filter(vm => vm.hostServer === server.id);
let currentY = START_Y + NODE_HEIGHT + VERTICAL_SPACING;
hostVMs.forEach(vm => {
const appCount = applications.filter(app => app.serverId === vm.id).length;
vmNodes.push({
id: `vm-${vm.id}`,
type: "vm",
data: {
label: `${vm.name}\n${vm.ip}`,
...vm,
},
position: {
x: serverX,
y: currentY,
},
style: {
background: "#fef2f2",
color: "#0f0f0f",
border: "2px solid #fecaca",
borderRadius: "4px",
padding: "6px",
width: APP_NODE_WIDTH,
height: APP_NODE_HEIGHT,
fontSize: "0.8rem",
lineHeight: "1.1",
whiteSpace: "pre-wrap",
},
});
// Dynamischer Abstand basierend auf Anzahl Apps
const requiredSpace = appCount > 0
? (appCount * (APP_NODE_HEIGHT + APP_ROW_SPACING))
: 0;
currentY += Math.max(
requiredSpace + MIN_VM_SPACING,
MIN_VM_SPACING + APP_NODE_HEIGHT
);
});
}
});
// Level 4: VM Applications (right column)
const vmAppNodes: Node[] = [];
vmNodes.forEach((vm) => {
const vmX = vm.position.x;
applications
.filter(app => app.serverId === vm.data.id)
.forEach((app, appIndex) => {
vmAppNodes.push({
id: `vm-app-${app.id}`,
type: "application",
data: {
label: `${app.name}\n${app.localURL}`,
...app,
},
position: {
x: vmX + VM_APP_SPACING,
y: vm.position.y + appIndex * (APP_NODE_HEIGHT + 20),
},
style: {
background: "#f5f5f5",
color: "#0f0f0f",
border: "2px solid #e6e4e1",
borderRadius: "4px",
padding: "6px",
width: APP_NODE_WIDTH,
height: APP_NODE_HEIGHT,
fontSize: "0.8rem",
lineHeight: "1.1",
whiteSpace: "pre-wrap",
},
});
});
});
// Calculate dimensions for root node positioning
const tempNodes = [...serverNodes, ...serviceNodes, ...vmNodes, ...vmAppNodes];
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
tempNodes.forEach((node) => {
const width = parseInt(node.style.width?.toString() || "0", 10);
const height = parseInt(node.style.height?.toString() || "0", 10);
minX = Math.min(minX, node.position.x);
maxX = Math.max(maxX, node.position.x + width);
minY = Math.min(minY, node.position.y);
maxY = Math.max(maxY, node.position.y + height);
});
const centerX = (minX + maxX) / 2;
const rootX = centerX - ROOT_NODE_WIDTH / 2;
// Level 1: Root Node (centered at top)
const rootNode: Node = {
id: "root",
type: "infrastructure",
data: { label: "My Infrastructure" },
position: { x: rootX, y: 0 },
style: {
background: "#ffffff",
color: "#0f0f0f",
border: "2px solid #e6e4e1",
borderRadius: "8px",
padding: "16px",
width: ROOT_NODE_WIDTH,
height: NODE_HEIGHT,
fontSize: "1.2rem",
fontWeight: "bold",
},
};
// Update dimensions with root node
const allNodes = [rootNode, ...tempNodes];
let newMinX = Math.min(minX, rootNode.position.x);
let newMaxX = Math.max(maxX, rootNode.position.x + ROOT_NODE_WIDTH);
let newMinY = Math.min(minY, rootNode.position.y);
let newMaxY = Math.max(maxY, rootNode.position.y + NODE_HEIGHT);
// Container Node
const containerNode: Node = {
id: 'container',
type: 'container',
data: { label: '' },
position: {
x: newMinX - CONTAINER_PADDING,
y: newMinY - CONTAINER_PADDING
},
style: {
width: newMaxX - newMinX + 2 * CONTAINER_PADDING,
height: newMaxY - newMinY + 2 * CONTAINER_PADDING,
background: 'transparent',
border: '2px dashed #e2e8f0',
borderRadius: '8px',
zIndex: 0,
},
draggable: false,
selectable: false,
zIndex: -1,
};
// Connections with hierarchical chaining
const connections: Edge[] = [];
// Root to Servers
serverNodes.forEach((server) => {
connections.push({
id: `conn-root-${server.id}`,
source: "root",
target: server.id,
type: "straight",
style: {
stroke: "#94a3b8",
strokeWidth: 2,
},
});
});
// Services chaining
const servicesByServer = new Map<number, Node[]>();
serviceNodes.forEach(service => {
const serverId = service.data.serverId;
if (!servicesByServer.has(serverId)) servicesByServer.set(serverId, []);
servicesByServer.get(serverId)!.push(service);
});
servicesByServer.forEach((services, serverId) => {
services.sort((a, b) => a.position.y - b.position.y);
services.forEach((service, index) => {
if (index === 0) {
connections.push({
id: `conn-service-${service.id}`,
source: `server-${serverId}`,
target: service.id,
type: "straight",
style: { stroke: "#60a5fa", strokeWidth: 2 },
});
} else {
const prevService = services[index - 1];
connections.push({
id: `conn-service-${service.id}-${prevService.id}`,
source: prevService.id,
target: service.id,
type: "straight",
style: { stroke: "#60a5fa", strokeWidth: 2 },
});
}
});
});
// VMs chaining
const vmsByHost = new Map<number, Node[]>();
vmNodes.forEach(vm => {
const hostId = vm.data.hostServer;
if (!vmsByHost.has(hostId)) vmsByHost.set(hostId, []);
vmsByHost.get(hostId)!.push(vm);
});
vmsByHost.forEach((vms, hostId) => {
vms.sort((a, b) => a.position.y - b.position.y);
vms.forEach((vm, index) => {
if (index === 0) {
connections.push({
id: `conn-vm-${vm.id}`,
source: `server-${hostId}`,
target: vm.id,
type: "straight",
style: { stroke: "#f87171", strokeWidth: 2 },
});
} else {
const prevVm = vms[index - 1];
connections.push({
id: `conn-vm-${vm.id}-${prevVm.id}`,
source: prevVm.id,
target: vm.id,
type: "straight",
style: { stroke: "#f87171", strokeWidth: 2 },
});
}
});
});
// VM Applications chaining
const appsByVM = new Map<number, Node[]>();
vmAppNodes.forEach(app => {
const vmId = app.data.serverId;
if (!appsByVM.has(vmId)) appsByVM.set(vmId, []);
appsByVM.get(vmId)!.push(app);
});
appsByVM.forEach((apps, vmId) => {
apps.sort((a, b) => a.position.y - b.position.y);
apps.forEach((app, index) => {
if (index === 0) {
connections.push({
id: `conn-vm-app-${app.id}`,
source: `vm-${vmId}`,
target: app.id,
type: "straight",
style: { stroke: "#f87171", strokeWidth: 2 },
});
} else {
const prevApp = apps[index - 1];
connections.push({
id: `conn-vm-app-${app.id}-${prevApp.id}`,
source: prevApp.id,
target: app.id,
type: "straight",
style: { stroke: "#f87171", strokeWidth: 2 },
});
}
});
});
return NextResponse.json({
nodes: [containerNode, ...allNodes],
edges: connections,
});
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json(
{
error: `Error fetching flowchart: ${errorMessage}`,
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,61 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface AddRequest {
type: string;
name: string;
smtpHost?: string;
smtpPort?: number;
smtpSecure?: boolean;
smtpUsername?: string;
smtpPassword?: string;
smtpFrom?: string;
smtpTo?: string;
telegramToken?: string;
telegramChatId?: string;
discordWebhook?: string;
gotifyUrl?: string;
gotifyToken?: string;
ntfyUrl?: string;
ntfyToken?: string;
pushoverUrl?: string;
pushoverToken?: string;
pushoverUser?: string;
echobellURL?: string;
}
export async function POST(request: NextRequest) {
try {
const body: AddRequest = await request.json();
const { type, name, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken, pushoverUrl, pushoverToken, pushoverUser, echobellURL } = body;
const notification = await prisma.notification.create({
data: {
type: type,
name: name,
smtpHost: smtpHost,
smtpPort: smtpPort,
smtpFrom: smtpFrom,
smtpUser: smtpUsername,
smtpPass: smtpPassword,
smtpSecure: smtpSecure,
smtpTo: smtpTo,
telegramChatId: telegramChatId,
telegramToken: telegramToken,
discordWebhook: discordWebhook,
gotifyUrl: gotifyUrl,
gotifyToken: gotifyToken,
ntfyUrl: ntfyUrl,
ntfyToken: ntfyToken,
pushoverUrl: pushoverUrl,
pushoverToken: pushoverToken,
pushoverUser: pushoverUser,
echobellURL: echobellURL
}
});
return NextResponse.json({ message: "Success", notification });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,21 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const id = Number(body.id);
if (!id) {
return NextResponse.json({ error: "Missing ID" }, { status: 400 });
}
await prisma.notification.delete({
where: { id: id }
});
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,16 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) {
try {
const notifications = await prisma.notification.findMany();
return NextResponse.json({
notifications
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,23 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface AddRequest {
notificationId: number;
}
export async function POST(request: NextRequest) {
try {
const body: AddRequest = await request.json();
const { notificationId } = body;
const notification = await prisma.test_notification.create({
data: {
notificationId: notificationId,
}
});
return NextResponse.json({ message: "Success", notification });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,48 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface AddRequest {
host: boolean;
hostServer: number;
name: string;
icon: string;
os: string;
ip: string;
url: string;
cpu: string;
gpu: string;
ram: string;
disk: string;
monitoring: boolean;
monitoringURL: string;
}
export async function POST(request: NextRequest) {
try {
const body: AddRequest = await request.json();
const { host, hostServer, name, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body;
const server = await prisma.server.create({
data: {
host,
hostServer,
name,
icon,
os,
ip,
url,
cpu,
gpu,
ram,
disk,
monitoring,
monitoringURL
}
});
return NextResponse.json({ message: "Success", server });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,35 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const id = Number(body.id);
if (!id) {
return NextResponse.json({ error: "Missing ID" }, { status: 400 });
}
// Check if there are any applications associated with the server
const applications = await prisma.application.findMany({
where: { serverId: id }
});
if (applications.length > 0) {
return NextResponse.json({ error: "Cannot delete server with associated applications" }, { status: 400 });
}
// Delete all server history records for this server
await prisma.server_history.deleteMany({
where: { serverId: id }
});
// Delete the server
await prisma.server.delete({
where: { id: id }
});
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,61 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface EditRequest {
host: boolean;
hostServer: number;
id: number;
name: string;
icon: string;
os: string;
ip: string;
url: string;
cpu: string;
gpu: string;
ram: string;
disk: string;
monitoring: boolean;
monitoringURL: string;
}
export async function PUT(request: NextRequest) {
try {
const body: EditRequest = await request.json();
const { host, hostServer, id, name, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body;
const existingServer = await prisma.server.findUnique({ where: { id } });
if (!existingServer) {
return NextResponse.json({ error: "Server not found" }, { status: 404 });
}
let newHostServer = hostServer;
if (hostServer === null) {
newHostServer = 0;
} else {
newHostServer = hostServer;
}
const updatedServer = await prisma.server.update({
where: { id },
data: {
host,
hostServer: newHostServer,
name,
icon,
os,
ip,
url,
cpu,
gpu,
ram,
disk,
monitoring,
monitoringURL
}
});
return NextResponse.json({ message: "Server updated", server: updatedServer });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,270 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import { Prisma } from "@prisma/client";
interface GetRequest {
page?: number;
ITEMS_PER_PAGE?: number;
timeRange?: '1h' | '1d' | '7d' | '30d';
serverId?: number;
}
const getTimeRange = (timeRange: '1h' | '1d' | '7d' | '30d' = '1h') => {
const now = new Date();
switch (timeRange) {
case '1d':
return {
start: new Date(now.getTime() - 24 * 60 * 60 * 1000),
end: now,
intervalMinutes: 15 // 15 minute intervals
};
case '7d':
return {
start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
end: now,
intervalMinutes: 60 // 1 hour intervals
};
case '30d':
return {
start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
end: now,
intervalMinutes: 240 // 4 hour intervals
};
case '1h':
default:
return {
start: new Date(now.getTime() - 60 * 60 * 1000),
end: now,
intervalMinutes: 1 // 1 minute intervals
};
}
};
const getIntervals = (timeRange: '1h' | '1d' | '7d' | '30d' = '1h') => {
const { start, end, intervalMinutes } = getTimeRange(timeRange);
let intervalCount: number;
switch (timeRange) {
case '1d':
intervalCount = 96; // 24 hours * 4 (15-minute intervals)
break;
case '7d':
intervalCount = 168; // 7 days * 24 hours
break;
case '30d':
intervalCount = 180; // 30 days * 6 (4-hour intervals)
break;
case '1h':
default:
intervalCount = 60;
break;
}
// Calculate the total time span in minutes
const totalMinutes = Math.floor((end.getTime() - start.getTime()) / (1000 * 60));
// Create equally spaced intervals
return Array.from({ length: intervalCount }, (_, i) => {
const minutesFromEnd = Math.floor(i * (totalMinutes / (intervalCount - 1)));
const d = new Date(end.getTime() - minutesFromEnd * 60 * 1000);
return d;
}).reverse(); // Return in chronological order
};
const parseUsageValue = (value: string | null): number => {
if (!value) return 0;
return Math.round(parseFloat(value.replace('%', '')) * 100) / 100;
};
export async function POST(request: NextRequest) {
try {
const body: GetRequest = await request.json();
const page = Math.max(1, body.page || 1);
const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 4;
const timeRange = body.timeRange || '1h';
const serverId = body.serverId;
// If serverId is provided, only fetch that specific server
const hostsQuery = serverId
? { id: serverId }
: { hostServer: 0 };
let hosts;
if (!serverId) {
hosts = await prisma.server.findMany({
where: hostsQuery,
orderBy: { name: 'asc' as Prisma.SortOrder },
skip: (page - 1) * ITEMS_PER_PAGE,
take: ITEMS_PER_PAGE,
});
} else {
hosts = await prisma.server.findMany({
where: hostsQuery,
orderBy: { name: 'asc' as Prisma.SortOrder },
});
}
const { start } = getTimeRange(timeRange);
const intervals = getIntervals(timeRange);
const hostsWithVms = await Promise.all(
hosts.map(async (host) => {
const vms = await prisma.server.findMany({
where: { hostServer: host.id },
orderBy: { name: 'asc' }
});
// Get server history for the host
const serverHistory = await prisma.server_history.findMany({
where: {
serverId: host.id,
createdAt: {
gte: start
}
},
orderBy: {
createdAt: 'asc'
}
});
// Process history data into intervals
const historyMap = new Map<string, {
cpu: number[],
ram: number[],
disk: number[],
gpu: number[],
temp: number[],
online: boolean[]
}>();
// Initialize intervals
intervals.forEach(date => {
const key = date.toISOString();
historyMap.set(key, {
cpu: [],
ram: [],
disk: [],
gpu: [],
temp: [],
online: []
});
});
// Group data by interval
serverHistory.forEach(record => {
const recordDate = new Date(record.createdAt);
let nearestInterval: Date = intervals[0];
let minDiff = Infinity;
// Find the nearest interval for this record
intervals.forEach(intervalDate => {
const diff = Math.abs(recordDate.getTime() - intervalDate.getTime());
if (diff < minDiff) {
minDiff = diff;
nearestInterval = intervalDate;
}
});
const key = nearestInterval.toISOString();
const interval = historyMap.get(key);
if (interval) {
interval.cpu.push(parseUsageValue(record.cpuUsage));
interval.ram.push(parseUsageValue(record.ramUsage));
interval.disk.push(parseUsageValue(record.diskUsage));
interval.gpu.push(parseUsageValue(record.gpuUsage));
interval.temp.push(parseUsageValue(record.temp));
interval.online.push(record.online);
}
});
// Calculate averages for each interval
const historyData = intervals.map(date => {
const key = date.toISOString();
const data = historyMap.get(key) || {
cpu: [],
ram: [],
disk: [],
gpu: [],
temp: [],
online: []
};
const average = (arr: number[]) =>
arr.length ? Math.round((arr.reduce((a, b) => a + b, 0) / arr.length) * 100) / 100 : null;
return {
timestamp: key,
cpu: average(data.cpu),
ram: average(data.ram),
disk: average(data.disk),
gpu: average(data.gpu),
temp: average(data.temp),
online: data.online.length ?
data.online.filter(Boolean).length / data.online.length >= 0.5
: null
};
});
// Add isVM flag to VMs
const vmsWithFlag = vms.map(vm => ({
...vm,
isVM: true,
hostedVMs: [] // Initialize empty hostedVMs array for VMs
}));
return {
...host,
isVM: false,
hostedVMs: vmsWithFlag,
history: {
labels: intervals.map(d => d.toISOString()),
datasets: {
cpu: intervals.map(d => {
const data = historyMap.get(d.toISOString())?.cpu || [];
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
}),
ram: intervals.map(d => {
const data = historyMap.get(d.toISOString())?.ram || [];
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
}),
disk: intervals.map(d => {
const data = historyMap.get(d.toISOString())?.disk || [];
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
}),
gpu: intervals.map(d => {
const data = historyMap.get(d.toISOString())?.gpu || [];
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
}),
temp: intervals.map(d => {
const data = historyMap.get(d.toISOString())?.temp || [];
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
}),
online: intervals.map(d => {
const data = historyMap.get(d.toISOString())?.online || [];
return data.length ? data.filter(Boolean).length / data.length >= 0.5 : null;
})
}
}
};
})
);
// Only calculate maxPage when not requesting a specific server
let maxPage = 1;
let totalHosts = 0;
if (!serverId) {
totalHosts = await prisma.server.count({
where: { OR: [{ hostServer: 0 }, { hostServer: null }] }
});
maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);
}
return NextResponse.json({
servers: hostsWithVms,
maxPage,
totalItems: totalHosts
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,21 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET(request: NextRequest) {
try {
const servers = await prisma.server.findMany({
where: { host: true },
});
// Add required properties to ensure consistency
const serversWithProps = servers.map(server => ({
...server,
isVM: false,
hostedVMs: [] // Initialize empty hostedVMs array
}));
return NextResponse.json({ servers: serversWithProps });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,44 @@
import { NextResponse } from "next/server"
import { prisma } from "@/lib/prisma";
export async function GET() {
try {
const servers = await prisma.server.findMany({
select: {
id: true,
online: true,
cpuUsage: true,
ramUsage: true,
diskUsage: true,
gpuUsage: true,
temp: true,
uptime: true
}
});
const monitoringData = servers.map((server: {
id: number;
online: boolean;
cpuUsage: string | null;
ramUsage: string | null;
diskUsage: string | null;
gpuUsage: string | null;
temp: string | null;
uptime: string | null;
}) => ({
id: server.id,
online: server.online,
cpuUsage: server.cpuUsage ? parseFloat(server.cpuUsage) : 0,
ramUsage: server.ramUsage ? parseFloat(server.ramUsage) : 0,
diskUsage: server.diskUsage ? parseFloat(server.diskUsage) : 0,
gpuUsage: server.gpuUsage ? parseFloat(server.gpuUsage) : 0,
temp: server.temp ? parseFloat(server.temp) : 0,
uptime: server.uptime || ""
}));
return NextResponse.json(monitoringData)
} catch (error) {
return new NextResponse("Internal Error", { status: 500 })
}
}

View File

@@ -0,0 +1,68 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import Fuse from "fuse.js";
interface SearchRequest {
searchterm: string;
}
export async function POST(request: NextRequest) {
try {
const body: SearchRequest = await request.json();
const { searchterm } = body;
// Fetch all servers
const servers = await prisma.server.findMany({});
// Create a map of host servers with their hosted VMs
const serverMap = new Map();
servers.forEach(server => {
if (server.host) {
serverMap.set(server.id, {
...server,
isVM: false,
hostedVMs: []
});
}
});
// Add VMs to their host servers and mark them as VMs
const serversWithType = servers.map(server => {
// If not a host and has a hostServer, it's a VM
if (!server.host && server.hostServer) {
const hostServer = serverMap.get(server.hostServer);
if (hostServer) {
hostServer.hostedVMs.push({
...server,
isVM: true
});
}
return {
...server,
isVM: true
};
}
return {
...server,
isVM: false,
hostedVMs: serverMap.get(server.id)?.hostedVMs || []
};
});
const fuseOptions = {
keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk', 'os'],
threshold: 0.3,
includeScore: true,
};
const fuse = new Fuse(serversWithType, fuseOptions);
const searchResults = fuse.search(searchterm);
const results = searchResults.map(({ item }) => item);
return NextResponse.json({ results });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,25 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) {
try {
// Check if there are any settings entries
const existingSettings = await prisma.settings.findFirst();
if (!existingSettings) {
return NextResponse.json({ "notification_text_application": "", "notification_text_server": "" });
}
// If settings entry exists, fetch it
const settings = await prisma.settings.findFirst({
where: { id: existingSettings.id },
});
if (!settings) {
return NextResponse.json({ "notification_text_application": "", "notification_text_server": "" });
}
// Return the settings entry
return NextResponse.json({ "notification_text_application": settings.notification_text_application, "notification_text_server": settings.notification_text_server });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,36 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface AddRequest {
text_application: string;
text_server: string;
}
export async function POST(request: NextRequest) {
try {
const body: AddRequest = await request.json();
const { text_application, text_server } = body;
// Check if there is already a settings entry
const existingSettings = await prisma.settings.findFirst();
if (existingSettings) {
// Update the existing settings entry
const updatedSettings = await prisma.settings.update({
where: { id: existingSettings.id },
data: { notification_text_application: text_application, notification_text_server: text_server },
});
return NextResponse.json({ message: "Success", updatedSettings });
}
// If no settings entry exists, create a new one
const settings = await prisma.settings.create({
data: {
notification_text_application: text_application,
notification_text_server: text_server,
}
});
return NextResponse.json({ message: "Success", settings });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}