diff --git a/README.md b/README.md index 3ba1298..add9013 100644 --- a/README.md +++ b/README.md @@ -17,25 +17,30 @@ The only dashboard you'll ever need to manage your entire server infrastructure. ## Screenshots Login Page: -![Login Page](https://i.ibb.co/QvvJvHxY/image.png) +![Login Page](https://i.ibb.co/DfS7BJdX/image.png) Dashboard Page: -![Dashboard Page](https://i.ibb.co/G3FW5mVX/image.png) +![Dashboard Page](https://i.ibb.co/m5xMXz73/image.png) Servers Page: -![Servers Page](https://i.ibb.co/v6Z79wJY/image.png) +![Servers Page](https://i.ibb.co/QFrFRp1B/image.png) Applications Page: -![Applications Page](https://i.ibb.co/zC1f6s9/image.png) +![Applications Page](https://i.ibb.co/1JK3pFYG/image.png) + +Uptime Page: +![Uptime Page](https://i.ibb.co/99LTnZ14/image.png) Network Page: -![Network Page](https://i.ibb.co/XkKYrGQX/image.png) +![Network Page](https://i.ibb.co/1Y6ypKHk/image.png) + +Settings Page: +![Settings Page](https://i.ibb.co/mrdjqy7f/image.png) ## Roadmap -- [ ] Edit Applications, Applications searchbar -- [ ] Customizable Dashboard +- [X] Edit Applications, Applications searchbar +- [X] Uptime History - [ ] Notifications -- [ ] Uptime History - [ ] Simple Server Monitoring - [ ] Improved Network Flowchart with custom elements (like Network switches) - [ ] Advanced Settings (Disable Uptime Tracking & more) @@ -50,10 +55,7 @@ services: ports: - "3000:3000" environment: - LOGIN_EMAIL: "mail@example.com" - LOGIN_PASSWORD: "SecretPassword" - JWT_SECRET: RANDOM_SECRET - ACCOUNT_SECRET: RANDOM_SECRET + JWT_SECRET: RANDOM_SECRET # Replace with a secure random string DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres" depends_on: - db @@ -78,6 +80,10 @@ volumes: postgres_data: ``` +#### Default Login +__E-Mail:__ admin@example.com\ +__Password:__ admin + ## Tech Stack & Credits The application is build with: @@ -87,6 +93,7 @@ The application is build with: - PostgreSQL with [Prisma ORM](https://www.prisma.io/) - Icons by [Lucide](https://lucide.dev/) - Flowcharts by [React Flow](https://reactflow.dev/) +- Application icons by [selfh.st/icons](selfh.st/icons) - and a lot of love ❤️ ## Star History diff --git a/agent/.dockerignore b/agent/.dockerignore index 3a8fe5e..2eea525 100644 --- a/agent/.dockerignore +++ b/agent/.dockerignore @@ -1 +1 @@ -.env.local \ No newline at end of file +.env \ No newline at end of file diff --git a/agent/main.go b/agent/main.go index 5619f9d..3fca4da 100644 --- a/agent/main.go +++ b/agent/main.go @@ -34,19 +34,48 @@ func main() { } defer db.Close() - ticker := time.NewTicker(5 * time.Second) + go func() { + deletionTicker := time.NewTicker(1 * time.Hour) + defer deletionTicker.Stop() + + for range deletionTicker.C { + if err := deleteOldEntries(db); err != nil { + fmt.Printf("Error deleting old entries: %v\n", err) + } + } + }() + + ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() client := &http.Client{ Timeout: 4 * time.Second, } - for range ticker.C { + for now := range ticker.C { + if now.Second()%10 != 0 { + continue + } + apps := getApplications(db) checkAndUpdateStatus(db, client, apps) } } +func deleteOldEntries(db *sql.DB) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + res, err := db.ExecContext(ctx, + `DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`) + if err != nil { + return err + } + affected, _ := res.RowsAffected() + fmt.Printf("Deleted %d old entries from uptime_history\n", affected) + return nil +} + func getApplications(db *sql.DB) []Application { rows, err := db.Query(` SELECT id, "publicURL", online @@ -90,12 +119,21 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { } _, err = db.ExecContext(ctx, - "UPDATE application SET online = $1 WHERE id = $2", + `UPDATE application SET online = $1 WHERE id = $2`, isOnline, app.ID, ) if err != nil { fmt.Printf("Update failed for app %d: %v\n", app.ID, err) } + + _, err = db.ExecContext(ctx, + `INSERT INTO uptime_history ("applicationId", online, "createdAt") VALUES ($1, $2, now())`, + app.ID, + isOnline, + ) + if err != nil { + fmt.Printf("Insert into uptime_history failed for app %d: %v\n", app.ID, err) + } } } diff --git a/app/api/applications/delete/route.ts b/app/api/applications/delete/route.ts index e6bfe2f..a3132f3 100644 --- a/app/api/applications/delete/route.ts +++ b/app/api/applications/delete/route.ts @@ -14,6 +14,10 @@ export async function POST(request: NextRequest) { 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 }); diff --git a/app/api/applications/uptime/route.ts b/app/api/applications/uptime/route.ts new file mode 100644 index 0000000..e9dfc67 --- /dev/null +++ b/app/api/applications/uptime/route.ts @@ -0,0 +1,162 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; + +interface RequestBody { + timespan?: number; + page?: number; + } + + +const getTimeRange = (timespan: number) => { + const now = new Date(); + switch (timespan) { + case 1: + return { + start: new Date(now.getTime() - 30 * 60 * 1000), + interval: 'minute' + }; + case 2: + return { + start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), + interval: '3hour' + }; + case 3: + return { + start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), + interval: 'day' + }; + default: + return { + start: new Date(now.getTime() - 30 * 60 * 1000), + interval: 'minute' + }; + } +}; + +const generateIntervals = (timespan: number) => { + const now = new Date(); + now.setSeconds(0, 0); + + switch (timespan) { + case 1: + return Array.from({ length: 30 }, (_, i) => { + const d = new Date(now); + d.setMinutes(d.getMinutes() - i); + d.setSeconds(0, 0); + return d; + }); + + case 2: + return Array.from({ length: 56 }, (_, i) => { + const d = new Date(now); + d.setHours(d.getHours() - (i * 3)); + d.setMinutes(0, 0, 0); + return d; + }); + + case 3: + 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: + d.setSeconds(0, 0); + return d.toISOString(); + case 2: + d.setHours(Math.floor(d.getHours() / 3) * 3); + d.setMinutes(0, 0, 0); + return d.toISOString(); + case 3: + 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 }: RequestBody = await request.json(); + const itemsPerPage = 5; + 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(); + + 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 }); + } + } \ No newline at end of file diff --git a/app/api/auth/edit_email/route.ts b/app/api/auth/edit_email/route.ts new file mode 100644 index 0000000..73fccd1 --- /dev/null +++ b/app/api/auth/edit_email/route.ts @@ -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 }); + } +} \ No newline at end of file diff --git a/app/api/auth/edit_password/route.ts b/app/api/auth/edit_password/route.ts new file mode 100644 index 0000000..dd162a8 --- /dev/null +++ b/app/api/auth/edit_password/route.ts @@ -0,0 +1,55 @@ +import { NextResponse, NextRequest } from "next/server"; +import jwt from 'jsonwebtoken'; +import { prisma } from "@/lib/prisma"; +import bcrypt from 'bcrypt'; + +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 }); + } +} \ No newline at end of file diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index af7a928..0e55791 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,5 +1,7 @@ import { NextResponse, NextRequest } from "next/server"; import jwt from 'jsonwebtoken'; +import { prisma } from "@/lib/prisma"; +import bcrypt from 'bcrypt'; interface LoginRequest { username: string; @@ -11,17 +13,50 @@ export async function POST(request: NextRequest) { const body: LoginRequest = await request.json(); const { username, password } = body; - if(username !== process.env.LOGIN_EMAIL || password !== process.env.LOGIN_PASSWORD) { - throw new Error('Invalid credentials'); - } - // 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: process.env.ACCOUNT_SECRET }, process.env.JWT_SECRET, { expiresIn: '7d' }); + const token = jwt.sign({ account_secret: accountId }, process.env.JWT_SECRET, { expiresIn: '7d' }); return NextResponse.json({ token }); } catch (error: any) { diff --git a/app/api/auth/validate/route.ts b/app/api/auth/validate/route.ts index 14ca8b3..0b25efa 100644 --- a/app/api/auth/validate/route.ts +++ b/app/api/auth/validate/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import jwt, { JwtPayload } from 'jsonwebtoken'; - +import { prisma } from "@/lib/prisma"; interface ValidateRequest { token: string; @@ -16,6 +16,14 @@ export async function POST(request: NextRequest) { 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 }; @@ -23,7 +31,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Invalid token' }, { status: 400 }); } - if(decoded.account_secret !== process.env.ACCOUNT_SECRET) { + if(decoded.account_secret !== user.id) { return NextResponse.json({ error: 'Invalid token' }, { status: 400 }); } diff --git a/app/api/flowchart/route.ts b/app/api/flowchart/route.ts index 455bff7..737dbfb 100644 --- a/app/api/flowchart/route.ts +++ b/app/api/flowchart/route.ts @@ -10,6 +10,9 @@ interface Node { }; position: { x: number; y: number }; style: React.CSSProperties; + draggable?: boolean; + selectable?: boolean; + zIndex?: number; } interface Edge { @@ -38,10 +41,13 @@ interface Application { const NODE_WIDTH = 220; const NODE_HEIGHT = 60; +const APP_NODE_WIDTH = 160; +const APP_NODE_HEIGHT = 40; const HORIZONTAL_SPACING = 280; -const VERTICAL_SPACING = 80; +const VERTICAL_SPACING = 60; const START_Y = 120; const ROOT_NODE_WIDTH = 300; +const CONTAINER_PADDING = 40; export async function GET() { try { @@ -54,11 +60,12 @@ export async function GET() { }) as Promise, ]); + // Root Node const rootNode: Node = { id: "root", type: "infrastructure", data: { label: "My Infrastructure" }, - position: { x: 0, y: 20 }, + position: { x: 0, y: 0 }, style: { background: "#ffffff", color: "#0f0f0f", @@ -72,6 +79,7 @@ export async function GET() { }, }; + // Server Nodes const serverNodes: Node[] = servers.map((server, index) => { const xPos = index * HORIZONTAL_SPACING - @@ -100,11 +108,12 @@ export async function GET() { }; }); + // Application Nodes const appNodes: Node[] = []; servers.forEach((server) => { - const serverX = - serverNodes.find((n) => n.id === `server-${server.id}`)?.position.x || 0; - const serverY = START_Y; + const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`); + const serverX = serverNode?.position.x || 0; + const xOffset = (NODE_WIDTH - APP_NODE_WIDTH) / 2; applications .filter((app) => app.serverId === server.id) @@ -117,25 +126,26 @@ export async function GET() { ...app, }, position: { - x: serverX, - y: serverY + NODE_HEIGHT + 40 + appIndex * VERTICAL_SPACING, + x: serverX + xOffset, + y: START_Y + NODE_HEIGHT + 30 + appIndex * VERTICAL_SPACING, }, style: { - background: "#ffffff", + background: "#f5f5f5", color: "#0f0f0f", border: "2px solid #e6e4e1", borderRadius: "4px", - padding: "8px", - width: NODE_WIDTH, - height: NODE_HEIGHT, - fontSize: "0.9rem", - lineHeight: "1.2", + padding: "6px", + width: APP_NODE_WIDTH, + height: APP_NODE_HEIGHT, + fontSize: "0.8rem", + lineHeight: "1.1", whiteSpace: "pre-wrap", }, }); }); }); + // Connections const connections: Edge[] = [ ...servers.map((server) => ({ id: `conn-root-${server.id}`, @@ -159,8 +169,46 @@ export async function GET() { })), ]; + // Container Box + const allNodes = [rootNode, ...serverNodes, ...appNodes]; + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + + allNodes.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 containerNode: Node = { + id: 'container', + type: 'container', + data: { label: '' }, + position: { + x: minX - CONTAINER_PADDING, + y: minY - CONTAINER_PADDING + }, + style: { + width: maxX - minX + 2 * CONTAINER_PADDING, + height: maxY - minY + 2 * CONTAINER_PADDING, + background: 'transparent', + border: '2px dashed #e2e8f0', + borderRadius: '8px', + zIndex: 0, + }, + draggable: false, + selectable: false, + zIndex: -1, + }; + return NextResponse.json({ - nodes: [rootNode, ...serverNodes, ...appNodes], + nodes: [containerNode, ...allNodes], edges: connections, }); } catch (error: unknown) { diff --git a/app/api/servers/delete/route.ts b/app/api/servers/delete/route.ts index 82265da..ed7b852 100644 --- a/app/api/servers/delete/route.ts +++ b/app/api/servers/delete/route.ts @@ -9,6 +9,14 @@ export async function POST(request: NextRequest) { 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 }); + } await prisma.server.delete({ where: { id: id } diff --git a/app/api/servers/get/route.ts b/app/api/servers/get/route.ts index 64fa480..bbeaf22 100644 --- a/app/api/servers/get/route.ts +++ b/app/api/servers/get/route.ts @@ -2,15 +2,16 @@ import { NextResponse, NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; interface GetRequest { - page: number; + page?: number; + ITEMS_PER_PAGE?: number; } -const ITEMS_PER_PAGE = 5; 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 servers = await prisma.server.findMany({ skip: (page - 1) * ITEMS_PER_PAGE, diff --git a/app/dashboard/Dashboard.tsx b/app/dashboard/Dashboard.tsx index ef5fc5b..b096e62 100644 --- a/app/dashboard/Dashboard.tsx +++ b/app/dashboard/Dashboard.tsx @@ -1,62 +1,61 @@ -import { AppSidebar } from "@/components/app-sidebar"; +"use client" + +import { useEffect, useState } from "react" +import axios from "axios" +import Link from "next/link" +import { Activity, Layers, Network, Server } from "lucide-react" + +import { AppSidebar } from "@/components/app-sidebar" import { Breadcrumb, BreadcrumbItem, - BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; -import { Separator } from "@/components/ui/separator"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar"; -import { useEffect, useState } from "react"; -import axios from "axios"; // Korrekter Import -import { Card, CardHeader } from "@/components/ui/card"; +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" interface StatsResponse { - serverCount: number; - applicationCount: number; - onlineApplicationsCount: number; + serverCount: number + applicationCount: number + onlineApplicationsCount: number } export default function Dashboard() { - const [serverCount, setServerCount] = useState(0); - const [applicationCount, setApplicationCount] = useState(0); - const [onlineApplicationsCount, setOnlineApplicationsCount] = useState(0); + const [serverCount, setServerCount] = useState(0) + const [applicationCount, setApplicationCount] = useState(0) + const [onlineApplicationsCount, setOnlineApplicationsCount] = useState(0) const getStats = async () => { try { - const response = await axios.post('/api/dashboard/get', {}); - setServerCount(response.data.serverCount); - setApplicationCount(response.data.applicationCount); - setOnlineApplicationsCount(response.data.onlineApplicationsCount); + const response = await axios.post("/api/dashboard/get", {}) + setServerCount(response.data.serverCount) + setApplicationCount(response.data.applicationCount) + setOnlineApplicationsCount(response.data.onlineApplicationsCount) } catch (error: any) { - console.log("Axios error:", error.response?.data); + console.log("Axios error:", error.response?.data) } - }; + } useEffect(() => { - getStats(); - }, []); + getStats() + }, []) return ( -
+
- - / - + / @@ -66,51 +65,105 @@ export default function Dashboard() {
-
-
- - -
-
- {serverCount} - Servers -
+
+

Dashboard

+ +
+ + +
+ Servers +
+ Manage your server infrastructure
+ +
{serverCount}
+

Active servers

+
+ + +
- - -
-
- {applicationCount} - Applications -
+ + + +
+ Applications +
+ Manage your deployed applications
+ +
{applicationCount}
+

Running applications

+
+ + +
- - -
-
- + + + +
+ Uptime + +
+ Monitor your service availability +
+ +
+
+ {onlineApplicationsCount}/{applicationCount} - Applications are online +
+ {applicationCount > 0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}% +
+
+
0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}%`, + }} + >
+
+

Online applications

- +
+ + + +
+ + + +
+ Network + +
+ Manage network configuration +
+ +
{serverCount + applicationCount}
+

Active connections

+
+ + +
-
-
- COMING SOON -
-
-
- COMING SOON -
- ); -} \ No newline at end of file + ) +} diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx index 0ac2871..f21e8cc 100644 --- a/app/dashboard/applications/Applications.tsx +++ b/app/dashboard/applications/Applications.tsx @@ -67,6 +67,12 @@ import { import Cookies from "js-cookie"; import { useState, useEffect } from "react"; import axios from "axios"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" interface Application { id: number; @@ -254,7 +260,7 @@ export default function Dashboard() { -
+
@@ -275,9 +281,9 @@ export default function Dashboard() {
-
+
- Your Applications + Your Applications
+ + + + + + + Generate Icon URL + + +
@@ -457,7 +472,9 @@ export default function Dashboard() { {app.description} -
+ {app.description && ( +
+ )} Server: {app.server || "No server"}
@@ -664,14 +681,14 @@ export default function Dashboard() {
)} -
+
1} + style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }} /> @@ -679,9 +696,9 @@ export default function Dashboard() { diff --git a/app/dashboard/network/Networks.tsx b/app/dashboard/network/Networks.tsx index 92d8e2c..27ebf71 100644 --- a/app/dashboard/network/Networks.tsx +++ b/app/dashboard/network/Networks.tsx @@ -41,7 +41,7 @@ export default function Dashboard() { -
+
(1); const [maxPage, setMaxPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(4); const [servers, setServers] = useState([]); const [isGridLayout, setIsGridLayout] = useState(false); const [loading, setLoading] = useState(true); @@ -126,7 +127,9 @@ export default function Dashboard() { useEffect(() => { const savedLayout = Cookies.get("layoutPreference-servers"); - setIsGridLayout(savedLayout === "grid"); + const layout_bool = savedLayout === "grid"; + setIsGridLayout(layout_bool); + setItemsPerPage(layout_bool ? 6 : 4); }, []); const toggleLayout = () => { @@ -137,6 +140,7 @@ export default function Dashboard() { path: "/", sameSite: "strict", }); + setItemsPerPage(newLayout ? 6 : 4); }; const add = async () => { @@ -164,6 +168,7 @@ export default function Dashboard() { "/api/servers/get", { page: currentPage, + ITEMS_PER_PAGE: itemsPerPage, } ); setServers(response.data.servers); @@ -176,7 +181,7 @@ export default function Dashboard() { useEffect(() => { getServers(); - }, [currentPage]); + }, [currentPage, itemsPerPage]); const handlePrevious = () => { setCurrentPage((prev) => Math.max(1, prev - 1)); @@ -260,8 +265,8 @@ export default function Dashboard() { -
-
+
+
@@ -281,9 +286,9 @@ export default function Dashboard() {
-
+
- Your Servers + Your Servers
@@ -514,7 +519,7 @@ export default function Dashboard() {
- IP: {server.ip || "Nicht angegeben"} + IP: {server.ip || "Not set"}
@@ -750,14 +755,14 @@ export default function Dashboard() {
)} -
+
1} + style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }} /> @@ -767,9 +772,9 @@ export default function Dashboard() { diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index b4d0ece..171935f 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -12,7 +12,7 @@ import { SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar"; -import { Card, CardHeader } from "@/components/ui/card"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { useTheme } from "next-themes"; import { Select, @@ -21,15 +21,121 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { Input } from "@/components/ui/input" +import { useState } from "react"; +import axios from "axios"; +import Cookies from "js-cookie"; +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { AlertCircle, Check, Palette, User } from "lucide-react"; export default function Settings() { const { theme, setTheme } = useTheme(); + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [oldPassword, setOldPassword] = useState("") + + const [emailError, setEmailError] = useState("") + const [passwordError, setPasswordError] = useState("") + const [emailErrorVisible, setEmailErrorVisible] = useState(false) + const [passwordErrorVisible, setPasswordErrorVisible] = useState(false) + + const [passwordSuccess, setPasswordSuccess] = useState(false) + const [emailSuccess, setEmailSuccess] = useState(false) + + const changeEmail = async () => { + setEmailErrorVisible(false); + setEmailSuccess(false); + setEmailError(""); + + if (!email) { + setEmailError("Email is required"); + setEmailErrorVisible(true); + setTimeout(() => { + setEmailErrorVisible(false); + setEmailError(""); + } + , 3000); + return; + } + try { + await axios.post('/api/auth/edit_email', { + newEmail: email, + jwtToken: Cookies.get('token') + }); + setEmailSuccess(true); + setEmail(""); + setTimeout(() => { + setEmailSuccess(false); + }, 3000); + } catch (error: any) { + setEmailError(error.response.data.error); + setEmailErrorVisible(true); + setTimeout(() => { + setEmailErrorVisible(false); + setEmailError(""); + }, 3000); + } + } + + const changePassword = async () => { + try { + if (password !== confirmPassword) { + setPasswordError("Passwords do not match"); + setPasswordErrorVisible(true); + setTimeout(() => { + setPasswordErrorVisible(false); + setPasswordError(""); + }, 3000); + return; + } + if (!oldPassword || !password || !confirmPassword) { + setPasswordError("All fields are required"); + setPasswordErrorVisible(true); + setTimeout(() => { + setPasswordErrorVisible(false); + setPasswordError(""); + }, 3000); + return; + } + + const response = await axios.post('/api/auth/edit_password', { + oldPassword: oldPassword, + newPassword: password, + jwtToken: Cookies.get('token') + }); + + if (response.status === 200) { + setPasswordSuccess(true); + setPassword(""); + setOldPassword(""); + setConfirmPassword(""); + setTimeout(() => { + setPasswordSuccess(false); + }, 3000); + } + } catch (error: any) { + setPasswordErrorVisible(true); + setPasswordError(error.response.data.error); + setTimeout(() => { + setPasswordErrorVisible(false); + setPasswordError(""); + }, 3000); + } + } return ( -
+
@@ -50,32 +156,142 @@ export default function Settings() {
-
- Settings -
- - - Theme - +
+
+ Settings +
+
+ + +
+ +

User Settings

+
+ +
+ Manage your user settings here. You can change your email, password, and other account settings. +
+ +
+
+
+

Change Email

+
+ + {emailErrorVisible && ( + + + Error + {emailError} + + )} + + {emailSuccess && ( + + + Success + Email changed successfully. + + )} + +
+ setEmail(e.target.value)} + className="h-11" + /> + +
+
+ +
+
+

Change Password

+
+ + {passwordErrorVisible && ( + + + Error + {passwordError} + + )} + + {passwordSuccess && ( + + + Success + Password changed successfully. + + )} + +
+ setOldPassword(e.target.value)} + className="h-11" + /> + setPassword(e.target.value)} + className="h-11" + /> + setConfirmPassword(e.target.value)} + className="h-11" + /> + +
+
+
+
+
+ + + +
+ +

Theme Settings

+
+
+ +
+ Select a theme for the application. You can choose between light, dark, or system theme. +
+ +
+ +
+
- ); -} \ No newline at end of file + ) +} diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx new file mode 100644 index 0000000..6bc8d7e --- /dev/null +++ b/app/dashboard/uptime/Uptime.tsx @@ -0,0 +1,296 @@ +import { AppSidebar } from "@/components/app-sidebar"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { Separator } from "@/components/ui/separator"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; +import { useEffect, useState } from "react"; +import axios from "axios"; +import { Card, CardHeader } from "@/components/ui/card"; +import * as Tooltip from "@radix-ui/react-tooltip"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationPrevious, + PaginationNext, + PaginationLink, +} from "@/components/ui/pagination"; + +const timeFormats = { + 1: (timestamp: string) => + new Date(timestamp).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: false + }), + 2: (timestamp: string) => { + const start = new Date(timestamp); + const end = new Date(start.getTime() + 3 * 60 * 60 * 1000); + return `${start.toLocaleDateString([], { day: '2-digit', month: 'short' })} + ${start.getHours().toString().padStart(2, '0')}:00 - + ${end.getHours().toString().padStart(2, '0')}:00`; + }, + 3: (timestamp: string) => + new Date(timestamp).toLocaleDateString([], { + day: '2-digit', + month: 'short' + }) +}; + +const minBoxWidths = { + 1: 24, + 2: 24, + 3: 24 +}; + +interface UptimeData { + appName: string; + appId: number; + uptimeSummary: { + timestamp: string; + missing: boolean; + online: boolean | null; + }[]; +} + +interface PaginationData { + currentPage: number; + totalPages: number; + totalItems: number; +} + +export default function Uptime() { + const [data, setData] = useState([]); + const [timespan, setTimespan] = useState<1 | 2 | 3>(1); + const [pagination, setPagination] = useState({ + currentPage: 1, + totalPages: 1, + totalItems: 0 + }); + const [isLoading, setIsLoading] = useState(false); + + const getData = async (selectedTimespan: number, page: number) => { + setIsLoading(true); + try { + const response = await axios.post<{ + data: UptimeData[]; + pagination: PaginationData; + }>("/api/applications/uptime", { + timespan: selectedTimespan, + page + }); + + setData(response.data.data); + setPagination(response.data.pagination); + } catch (error) { + console.error("Error:", error); + setData([]); + setPagination({ + currentPage: 1, + totalPages: 1, + totalItems: 0 + }); + } finally { + setIsLoading(false); + } + }; + + const handlePrevious = () => { + const newPage = Math.max(1, pagination.currentPage - 1); + setPagination(prev => ({...prev, currentPage: newPage})); + getData(timespan, newPage); + }; + + const handleNext = () => { + const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1); + setPagination(prev => ({...prev, currentPage: newPage})); + getData(timespan, newPage); + }; + + useEffect(() => { + getData(timespan, 1); + }, [timespan]); + + return ( + + + +
+
+ + + + + + / + + + + My Infrastructure + + + + Uptime + + + +
+
+
+
+ Uptime + +
+ +
+ {isLoading ? ( +
Loading...
+ ) : ( + data.map((app) => { + const reversedSummary = [...app.uptimeSummary].reverse(); + const startTime = reversedSummary[0]?.timestamp; + const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp; + + return ( + + +
+
+ {app.appName} +
+ +
+
+ {startTime ? timeFormats[timespan](startTime) : ""} + {endTime ? timeFormats[timespan](endTime) : ""} +
+ + +
+ {reversedSummary.map((entry) => ( + + +
+ + + +
+

+ {timespan === 2 ? ( + timeFormats[2](entry.timestamp) + ) : ( + new Date(entry.timestamp).toLocaleString([], { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: timespan === 3 ? undefined : '2-digit', + hour12: false + }) + )} +

+

+ {entry.missing + ? "No data" + : entry.online + ? "Online" + : "Offline"} +

+
+ +
+
+ + ))} +
+ +
+
+ + + ); + }) + )} +
+ + {pagination.totalItems > 0 && !isLoading && ( +
+ + + + + + + {pagination.currentPage} + + + + + + +
+ )} +
+ + + ); +} \ No newline at end of file diff --git a/app/dashboard/uptime/page.tsx b/app/dashboard/uptime/page.tsx new file mode 100644 index 0000000..e397f3a --- /dev/null +++ b/app/dashboard/uptime/page.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Cookies from "js-cookie"; +import { useRouter } from "next/navigation"; +import Uptime from "./Uptime"; +import axios from "axios"; + +export default function DashboardPage() { + const router = useRouter(); + const [isAuthChecked, setIsAuthChecked] = useState(false); + const [isValid, setIsValid] = useState(false); + + useEffect(() => { + const token = Cookies.get("token"); + if (!token) { + router.push("/"); + } else { + const checkToken = async () => { + try { + const response = await axios.post("/api/auth/validate", { + token: token, + }); + + if (response.status === 200) { + setIsValid(true); + } + } catch (error: any) { + Cookies.remove("token"); + router.push("/"); + } + } + checkToken(); + } + setIsAuthChecked(true); + }, [router]); + + if (!isAuthChecked) { + return ( +
+
+ + + + + + + + + + + Loading... +
+
+ ) + } + + return isValid ? : null; +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 44bef4d..e03f7b5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,108 +1,148 @@ -"use client"; +"use client" + +import type React from "react" + +import { useState, useEffect } from "react" +import Cookies from "js-cookie" +import { useRouter } from "next/navigation" +import axios from "axios" +import { AlertCircle, KeyRound, Mail, User } from "lucide-react" +import Link from "next/link" import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { AlertCircle } from "lucide-react" - -import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert" - -import { useState, useEffect } from "react"; -import Cookies from "js-cookie"; -import { useRouter } from "next/navigation"; -import axios from "axios"; - +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" export default function Home() { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const router = useRouter(); - const [error, setError] = useState(''); - const [errorVisible, setErrorVisible] = useState(false); + const [username, setUsername] = useState("") + const [password, setPassword] = useState("") + const [rememberMe, setRememberMe] = useState(false) + const router = useRouter() + const [error, setError] = useState("") + const [errorVisible, setErrorVisible] = useState(false) + const [isLoading, setIsLoading] = useState(false) useEffect(() => { - const token = Cookies.get('token'); + const token = Cookies.get("token") if (token) { - router.push('/dashboard'); + router.push("/dashboard") } - }, [router]); + }, [router]) interface LoginResponse { - token: string; + token: string } const login = async () => { - try { - const response = await axios.post('/api/auth/login', { username, password }); - const { token } = response.data as LoginResponse; - Cookies.set('token', token); - router.push('/dashboard'); - } catch (error: any) { - setError(error.response.data.error); + if (!username || !password) { + setError("Please enter both email and password") setErrorVisible(true) + return + } + + try { + setIsLoading(true) + const response = await axios.post("/api/auth/login", { username, password }) + const { token } = response.data as LoginResponse + + const cookieOptions = rememberMe ? { expires: 7 } : {} + Cookies.set("token", token, cookieOptions) + + router.push("/dashboard") + } catch (error: any) { + setError(error.response?.data?.error || "Login failed. Please try again.") + setErrorVisible(true) + } finally { + setIsLoading(false) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + login() } } return ( -
- - - Login - - Enter your Login data of the compose.yml file below to access - - - - {errorVisible && ( - <> -
- - - Error - - {error} - - -
- - )} - -
-
-
- - setUsername(e.target.value)} - /> -
-
-
- -
- setPassword(e.target.value)}/> -
- +
+
+
+
+
+
- - +

CoreControl

+

Sign in to access your dashboard

+
+ + + + Login + Enter your credentials to continue + + + + {errorVisible && ( + + + Authentication Error + {error} + + )} + +
+
+ +
+ + setUsername(e.target.value)} + onKeyDown={handleKeyDown} + required + /> +
+
+ +
+
+ +
+
+ + setPassword(e.target.value)} + onKeyDown={handleKeyDown} + required + /> +
+
+
+
+ + + + +
+
) } diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 19a4f3d..a616da3 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -1,6 +1,6 @@ import * as React from "react" import Image from "next/image" -import { AppWindow, Settings, LayoutDashboardIcon, Briefcase, Server, Network } from "lucide-react" +import { AppWindow, Settings, LayoutDashboardIcon, Briefcase, Server, Network, Activity } from "lucide-react" import { Sidebar, SidebarContent, @@ -50,6 +50,11 @@ const data: { navMain: NavItem[] } = { icon: AppWindow, url: "/dashboard/applications", }, + { + title: "Uptime", + icon: Activity, + url: "/dashboard/uptime", + }, { title: "Network", icon: Network, diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..4a8cca4 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/compose.yml b/compose.yml index a7a10df..7f61602 100644 --- a/compose.yml +++ b/compose.yml @@ -4,10 +4,7 @@ services: ports: - "3000:3000" environment: - LOGIN_EMAIL: "mail@example.com" - LOGIN_PASSWORD: "SecretPassword" - JWT_SECRET: RANDOM_SECRET - ACCOUNT_SECRET: RANDOM_SECRET + JWT_SECRET: RANDOM_SECRET # Replace with a secure random string DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres" depends_on: - db diff --git a/package-lock.json b/package-lock.json index 50d007a..b4859da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "corecontrol", - "version": "0.1.0", + "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "corecontrol", - "version": "0.1.0", + "version": "0.0.4", "dependencies": { "@prisma/client": "^6.6.0", "@prisma/extension-accelerate": "^1.3.0", + "@radix-ui/react-accordion": "^1.2.4", "@radix-ui/react-alert-dialog": "^1.1.7", "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7", @@ -20,10 +21,12 @@ "@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tooltip": "^1.2.0", "@types/axios": "^0.9.36", + "@types/bcrypt": "^5.0.2", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^9.0.9", "@xyflow/react": "^12.5.5", "axios": "^1.8.4", + "bcrypt": "^5.1.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "fuse.js": "^7.1.0", @@ -62,9 +65,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.0.tgz", - "integrity": "sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.1.tgz", + "integrity": "sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==", "license": "MIT", "optional": true, "dependencies": { @@ -911,6 +914,26 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@next/env": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.0.tgz", @@ -1029,22 +1052,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz", - "integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@prisma/client": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.6.0.tgz", @@ -1151,6 +1158,37 @@ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.4.tgz", + "integrity": "sha512-SGCxlSBaMvEzDROzyZjsVNzu9XY5E28B3k8jOENyrz6csOv/pG1eHyYfLJai1n9tRjwG61coXDhfpgtxKxUv5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.4", + "@radix-ui/react-collection": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-controllable-state": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-alert-dialog": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.7.tgz", @@ -1202,6 +1240,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.4.tgz", + "integrity": "sha512-u7LCw1EYInQtBNLGjm9nZ89S/4GcvX1UR5XbekEgnQae2Hkpq39ycJ1OhdeN1/JDfVNG91kWaWoest127TaEKQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-controllable-state": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.3.tgz", @@ -2135,7 +2203,7 @@ "node": ">= 10" } }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "node_modules/@tailwindcss/oxide/node_modules/@tailwindcss/oxide-win32-x64-msvc": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.3.tgz", "integrity": "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==", @@ -2172,6 +2240,15 @@ "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", "license": "MIT" }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", @@ -2253,9 +2330,9 @@ } }, "node_modules/@types/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", - "integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz", + "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2273,12 +2350,12 @@ } }, "node_modules/@xyflow/react": { - "version": "12.5.5", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.5.5.tgz", - "integrity": "sha512-mAtHuS4ktYBL1ph5AJt7X/VmpzzlmQBN3+OXxyT/1PzxwrVto6AKc3caerfxzwBsg3cA4J8lB63F3WLAuPMmHw==", + "version": "12.5.6", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.5.6.tgz", + "integrity": "sha512-a6lL0WoeMSp7AC9AQzWMRMuqk12Dn+lVjMDLL93SZvpWv5D2BSq9woCv21JCUdWQ31MNpJVfLaV3TycaH1tsYw==", "license": "MIT", "dependencies": { - "@xyflow/system": "0.0.55", + "@xyflow/system": "0.0.56", "classcat": "^5.0.3", "zustand": "^4.4.0" }, @@ -2288,9 +2365,9 @@ } }, "node_modules/@xyflow/system": { - "version": "0.0.55", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.55.tgz", - "integrity": "sha512-6cngWlE4oMXm+zrsbJxerP3wUNUFJcv/cE5kDfu0qO55OWK3fAeSOLW9td3xEVQlomjIW5knds1MzeMnBeCfqw==", + "version": "0.0.56", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.56.tgz", + "integrity": "sha512-Xc3LvEumjJD+CqPqlYkrlszJ4hWQ0DE+r5M4e5WpS/hKT4T6ktAjt7zeMNJ+vvTsXHulGnEoDRA8zbIfB6tPdQ==", "license": "MIT", "dependencies": { "@types/d3-drag": "^3.0.7", @@ -2302,6 +2379,53 @@ "d3-zoom": "^3.0.0" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-hidden": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", @@ -2331,6 +2455,36 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2381,6 +2535,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -2459,6 +2622,15 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2471,6 +2643,18 @@ "node": ">= 0.8" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2587,7 +2771,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2610,11 +2793,16 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2649,6 +2837,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -2797,6 +2991,36 @@ "node": ">= 6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2830,6 +3054,27 @@ "node": ">=10" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2889,6 +3134,27 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2935,6 +3201,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2947,6 +3219,36 @@ "node": ">= 0.4" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -2954,6 +3256,15 @@ "license": "MIT", "optional": true }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -3234,7 +3545,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-win32-x64-msvc": { + "node_modules/lightningcss/node_modules/lightningcss-win32-x64-msvc": { "version": "1.29.2", "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", @@ -3306,6 +3617,30 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3336,6 +3671,64 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3424,6 +3817,22 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/next/node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz", + "integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -3452,6 +3861,87 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3612,6 +4102,20 @@ } } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -3622,6 +4126,22 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3660,6 +4180,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.34.1", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", @@ -3701,6 +4227,12 @@ "@img/sharp-win32-x64": "0.34.1" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -3728,6 +4260,41 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -3778,6 +4345,29 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3885,6 +4475,49 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/zustand": { "version": "4.5.6", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", @@ -3912,6 +4545,21 @@ "optional": true } } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz", + "integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/package.json b/package.json index ac39468..72dbb10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "corecontrol", - "version": "0.0.3", + "version": "0.0.4", "private": true, "scripts": { "dev": "next dev --turbopack", @@ -11,6 +11,7 @@ "dependencies": { "@prisma/client": "^6.6.0", "@prisma/extension-accelerate": "^1.3.0", + "@radix-ui/react-accordion": "^1.2.4", "@radix-ui/react-alert-dialog": "^1.1.7", "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7", @@ -21,10 +22,12 @@ "@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tooltip": "^1.2.0", "@types/axios": "^0.9.36", + "@types/bcrypt": "^5.0.2", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^9.0.9", "@xyflow/react": "^12.5.5", "axios": "^1.8.4", + "bcrypt": "^5.1.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "fuse.js": "^7.1.0", diff --git a/prisma/migrations/20250414173201_user_model/migration.sql b/prisma/migrations/20250414173201_user_model/migration.sql new file mode 100644 index 0000000..b81375d --- /dev/null +++ b/prisma/migrations/20250414173201_user_model/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "user" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + + CONSTRAINT "user_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_email_key" ON "user"("email"); diff --git a/prisma/migrations/20250414201604_uptime_history/migration.sql b/prisma/migrations/20250414201604_uptime_history/migration.sql new file mode 100644 index 0000000..3021ba4 --- /dev/null +++ b/prisma/migrations/20250414201604_uptime_history/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "uptime_history" ( + "id" SERIAL NOT NULL, + "applicationId" INTEGER NOT NULL DEFAULT 1, + "online" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "uptime_history_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cad3932..55fe57d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,6 +25,13 @@ model application { online Boolean @default(true) } +model uptime_history { + id Int @id @default(autoincrement()) + applicationId Int @default(1) + online Boolean @default(true) + createdAt DateTime @default(now()) +} + model server { id Int @id @default(autoincrement()) name String @@ -38,6 +45,12 @@ model server { } model settings { - id Int @id @default(autoincrement()) - uptime_checks Boolean @default(true) + id Int @id @default(autoincrement()) + uptime_checks Boolean @default(true) +} + +model user { + id String @id @default(uuid()) + email String @unique + password String } \ No newline at end of file diff --git a/public/cover.png b/public/cover.png new file mode 100644 index 0000000..e1b7b40 Binary files /dev/null and b/public/cover.png differ