mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-18 16:07:10 +00:00
v0.0.4 dev->main
v0.0.4
This commit is contained in:
commit
676354f53c
31
README.md
31
README.md
@ -17,25 +17,30 @@ The only dashboard you'll ever need to manage your entire server infrastructure.
|
||||
|
||||
## Screenshots
|
||||
Login Page:
|
||||

|
||||

|
||||
|
||||
Dashboard Page:
|
||||

|
||||

|
||||
|
||||
Servers Page:
|
||||

|
||||

|
||||
|
||||
Applications Page:
|
||||

|
||||

|
||||
|
||||
Uptime Page:
|
||||

|
||||
|
||||
Network Page:
|
||||

|
||||

|
||||
|
||||
Settings Page:
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
@ -1 +1 @@
|
||||
.env.local
|
||||
.env
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
|
||||
162
app/api/applications/uptime/route.ts
Normal file
162
app/api/applications/uptime/route.ts
Normal file
@ -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<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 });
|
||||
}
|
||||
}
|
||||
56
app/api/auth/edit_email/route.ts
Normal file
56
app/api/auth/edit_email/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
55
app/api/auth/edit_password/route.ts
Normal file
55
app/api/auth/edit_password/route.ts
Normal file
@ -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 });
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@ -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<Application[]>,
|
||||
]);
|
||||
|
||||
// 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) {
|
||||
|
||||
@ -10,6 +10,14 @@ export async function POST(request: NextRequest) {
|
||||
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 }
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<number>(0);
|
||||
const [applicationCount, setApplicationCount] = useState<number>(0);
|
||||
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0);
|
||||
const [serverCount, setServerCount] = useState<number>(0)
|
||||
const [applicationCount, setApplicationCount] = useState<number>(0)
|
||||
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0)
|
||||
|
||||
const getStats = async () => {
|
||||
try {
|
||||
const response = await axios.post<StatsResponse>('/api/dashboard/get', {});
|
||||
setServerCount(response.data.serverCount);
|
||||
setApplicationCount(response.data.applicationCount);
|
||||
setOnlineApplicationsCount(response.data.onlineApplicationsCount);
|
||||
const response = await axios.post<StatsResponse>("/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 (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbPage>
|
||||
/
|
||||
</BreadcrumbPage>
|
||||
<BreadcrumbPage>/</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
@ -66,51 +65,105 @@ export default function Dashboard() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="pl-4 pr-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<Card className="w-full mb-4 relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="text-2xl font-bold">{serverCount}</span>
|
||||
<span className="text-md">Servers</span>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-6">Dashboard</h1>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="overflow-hidden border-t-4 border-t-rose-500 shadow-sm transition-all hover:shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-medium">Servers</CardTitle>
|
||||
<Server className="h-6 w-6 text-rose-500" />
|
||||
</div>
|
||||
<CardDescription>Manage your server infrastructure</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2 pb-4">
|
||||
<div className="text-4xl font-bold">{serverCount}</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Active servers</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/20 p-4">
|
||||
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
|
||||
<Link href="/dashboard/servers">View all servers</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="w-full mb-4 relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="text-2xl font-bold">{applicationCount}</span>
|
||||
<span className="text-md">Applications</span>
|
||||
</div>
|
||||
|
||||
<Card className="overflow-hidden border-t-4 border-t-amber-500 shadow-sm transition-all hover:shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-medium">Applications</CardTitle>
|
||||
<Layers className="h-6 w-6 text-amber-500" />
|
||||
</div>
|
||||
<CardDescription>Manage your deployed applications</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2 pb-4">
|
||||
<div className="text-4xl font-bold">{applicationCount}</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Running applications</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/20 p-4">
|
||||
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
|
||||
<Link href="/dashboard/applications">View all applications</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="w-full mb-4 relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="text-2xl font-bold">
|
||||
|
||||
<Card className="overflow-hidden border-t-4 border-t-emerald-500 shadow-sm transition-all hover:shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-medium">Uptime</CardTitle>
|
||||
<Activity className="h-6 w-6 text-emerald-500" />
|
||||
</div>
|
||||
<CardDescription>Monitor your service availability</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2 pb-4">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-4xl font-bold flex items-center justify-between">
|
||||
<span>
|
||||
{onlineApplicationsCount}/{applicationCount}
|
||||
</span>
|
||||
<span className="text-md">Applications are online</span>
|
||||
<div className="flex items-center bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-lg font-semibold">
|
||||
{applicationCount > 0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-3">
|
||||
<div
|
||||
className="bg-emerald-500 h-2.5 rounded-full"
|
||||
style={{
|
||||
width: `${applicationCount > 0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Online applications</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/20 p-4">
|
||||
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
|
||||
<Link href="/dashboard/uptime">View uptime metrics</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden border-t-4 border-t-sky-500 shadow-sm transition-all hover:shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-medium">Network</CardTitle>
|
||||
<Network className="h-6 w-6 text-sky-500" />
|
||||
</div>
|
||||
<CardDescription>Manage network configuration</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2 pb-4">
|
||||
<div className="text-4xl font-bold">{serverCount + applicationCount}</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Active connections</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/20 p-4">
|
||||
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
|
||||
<Link href="/dashboard/network">View network details</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="h-72 w-full rounded-xl flex items-center justify-center bg-muted">
|
||||
<span className="text-gray-400 text-2xl">COMING SOON</span>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="h-72 w-full rounded-xl flex items-center justify-center bg-muted">
|
||||
<span className="text-gray-400 text-2xl">COMING SOON</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
)
|
||||
}
|
||||
@ -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() {
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
@ -275,9 +281,9 @@ export default function Dashboard() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="pl-4 pr-4">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-2xl font-semibold">Your Applications</span>
|
||||
<span className="text-3xl font-bold">Your Applications</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -358,9 +364,18 @@ export default function Dashboard() {
|
||||
placeholder="https://example.com/icon.png"
|
||||
onChange={(e) => setIcon(e.target.value)}
|
||||
/>
|
||||
<Button variant="outline" size="icon" onClick={generateIconURL}>
|
||||
<Zap />
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="outline" size="icon" onClick={generateIconURL}>
|
||||
<Zap />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Generate Icon URL
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
@ -457,7 +472,9 @@ export default function Dashboard() {
|
||||
</CardTitle>
|
||||
<CardDescription className="text-md">
|
||||
{app.description}
|
||||
<br />
|
||||
{app.description && (
|
||||
<br className="hidden md:block" />
|
||||
)}
|
||||
Server: {app.server || "No server"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
@ -664,14 +681,14 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-4">
|
||||
<div className="pt-4 pb-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={handlePrevious}
|
||||
isActive={currentPage > 1}
|
||||
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
@ -679,9 +696,9 @@ export default function Dashboard() {
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={handleNext}
|
||||
isActive={currentPage < maxPage}
|
||||
style={{ cursor: currentPage === maxPage ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
|
||||
@ -41,7 +41,7 @@ export default function Dashboard() {
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset className="flex flex-col h-screen">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1 dark:text-white" />
|
||||
<Separator
|
||||
|
||||
@ -107,6 +107,7 @@ export default function Dashboard() {
|
||||
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [maxPage, setMaxPage] = useState<number>(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(4);
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
const [isGridLayout, setIsGridLayout] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(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() {
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<Breadcrumb>
|
||||
@ -281,9 +286,9 @@ export default function Dashboard() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="pl-4 pr-4">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-2xl font-semibold">Your Servers</span>
|
||||
<span className="text-3xl font-bold">Your Servers</span>
|
||||
<div className="flex gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@ -514,7 +519,7 @@ export default function Dashboard() {
|
||||
<div className="flex items-center gap-2 text-foreground/80">
|
||||
<FileDigit className="h-4 w-4 text-muted-foreground" />
|
||||
<span>
|
||||
<b>IP:</b> {server.ip || "Nicht angegeben"}
|
||||
<b>IP:</b> {server.ip || "Not set"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -750,14 +755,14 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-4">
|
||||
<div className="pt-4 pb-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={handlePrevious}
|
||||
isActive={currentPage > 1}
|
||||
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
@ -767,9 +772,9 @@ export default function Dashboard() {
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={handleNext}
|
||||
isActive={currentPage < maxPage}
|
||||
style={{ cursor: currentPage === maxPage ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
|
||||
@ -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<string>("")
|
||||
const [password, setPassword] = useState<string>("")
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>("")
|
||||
const [oldPassword, setOldPassword] = useState<string>("")
|
||||
|
||||
const [emailError, setEmailError] = useState<string>("")
|
||||
const [passwordError, setPasswordError] = useState<string>("")
|
||||
const [emailErrorVisible, setEmailErrorVisible] = useState<boolean>(false)
|
||||
const [passwordErrorVisible, setPasswordErrorVisible] = useState<boolean>(false)
|
||||
|
||||
const [passwordSuccess, setPasswordSuccess] = useState<boolean>(false)
|
||||
const [emailSuccess, setEmailSuccess] = useState<boolean>(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 (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
@ -50,32 +156,142 @@ export default function Settings() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="pl-4 pr-4">
|
||||
<span className="text-2xl font-semibold">Settings</span>
|
||||
<div className="pt-4">
|
||||
<Card className="w-full mb-4 relative">
|
||||
<CardHeader>
|
||||
<span className="text-xl font-bold">Theme</span>
|
||||
<Select
|
||||
value={theme}
|
||||
onValueChange={(value: string) => setTheme(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full [&_svg]:hidden">
|
||||
<SelectValue>
|
||||
{(theme ?? 'system').charAt(0).toUpperCase() + (theme ?? 'system').slice(1)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="p-6">
|
||||
<div className="pb-4">
|
||||
<span className="text-3xl font-bold">Settings</span>
|
||||
</div>
|
||||
<div className="grid gap-6">
|
||||
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
|
||||
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-xl font-semibold">User Settings</h2>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<div className="text-sm text-muted-foreground mb-6">
|
||||
Manage your user settings here. You can change your email, password, and other account settings.
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div className="space-y-4">
|
||||
<div className="border-b pb-2">
|
||||
<h3 className="font-semibold text-lg">Change Email</h3>
|
||||
</div>
|
||||
|
||||
{emailErrorVisible && (
|
||||
<Alert variant="destructive" className="animate-in fade-in-50">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{emailError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{emailSuccess && (
|
||||
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
|
||||
<Check className="h-4 w-4" />
|
||||
<AlertTitle>Success</AlertTitle>
|
||||
<AlertDescription>Email changed successfully.</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter new email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Button onClick={changeEmail} className="w-full h-11">
|
||||
Change Email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="border-b pb-2">
|
||||
<h3 className="font-semibold text-lg">Change Password</h3>
|
||||
</div>
|
||||
|
||||
{passwordErrorVisible && (
|
||||
<Alert variant="destructive" className="animate-in fade-in-50">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{passwordError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{passwordSuccess && (
|
||||
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
|
||||
<Check className="h-4 w-4" />
|
||||
<AlertTitle>Success</AlertTitle>
|
||||
<AlertDescription>Password changed successfully.</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter old password"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Button onClick={changePassword} className="w-full h-11">
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
|
||||
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-xl font-semibold">Theme Settings</h2>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<div className="text-sm text-muted-foreground mb-6">
|
||||
Select a theme for the application. You can choose between light, dark, or system theme.
|
||||
</div>
|
||||
|
||||
<div className="max-w-md">
|
||||
<Select value={theme} onValueChange={(value: string) => setTheme(value)}>
|
||||
<SelectTrigger className="w-full h-11">
|
||||
<SelectValue>
|
||||
{(theme ?? "system").charAt(0).toUpperCase() + (theme ?? "system").slice(1)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
)
|
||||
}
|
||||
296
app/dashboard/uptime/Uptime.tsx
Normal file
296
app/dashboard/uptime/Uptime.tsx
Normal file
@ -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<UptimeData[]>([]);
|
||||
const [timespan, setTimespan] = useState<1 | 2 | 3>(1);
|
||||
const [pagination, setPagination] = useState<PaginationData>({
|
||||
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 (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbPage>/</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Uptime</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-3xl font-bold">Uptime</span>
|
||||
<Select
|
||||
value={String(timespan)}
|
||||
onValueChange={(v) => {
|
||||
setTimespan(Number(v) as 1 | 2 | 3);
|
||||
setPagination(prev => ({...prev, currentPage: 1}));
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select timespan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Last 30 minutes</SelectItem>
|
||||
<SelectItem value="2">Last 7 days</SelectItem>
|
||||
<SelectItem value="3">Last 30 days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">Loading...</div>
|
||||
) : (
|
||||
data.map((app) => {
|
||||
const reversedSummary = [...app.uptimeSummary].reverse();
|
||||
const startTime = reversedSummary[0]?.timestamp;
|
||||
const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp;
|
||||
|
||||
return (
|
||||
<Card key={app.appId}>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-semibold">{app.appName}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>{startTime ? timeFormats[timespan](startTime) : ""}</span>
|
||||
<span>{endTime ? timeFormats[timespan](endTime) : ""}</span>
|
||||
</div>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<div
|
||||
className="grid gap-0.5 w-full pb-2"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${minBoxWidths[timespan]}px, 1fr))`
|
||||
}}
|
||||
>
|
||||
{reversedSummary.map((entry) => (
|
||||
<Tooltip.Root key={entry.timestamp}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div
|
||||
className={`h-8 w-full rounded-sm border transition-colors ${
|
||||
entry.missing
|
||||
? "bg-gray-300 border-gray-400"
|
||||
: entry.online
|
||||
? "bg-green-500 border-green-600"
|
||||
: "bg-red-500 border-red-600"
|
||||
}`}
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="rounded bg-gray-900 px-2 py-1 text-white text-xs shadow-lg"
|
||||
side="top"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-medium">
|
||||
{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
|
||||
})
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{entry.missing
|
||||
? "No data"
|
||||
: entry.online
|
||||
? "Online"
|
||||
: "Offline"}
|
||||
</p>
|
||||
</div>
|
||||
<Tooltip.Arrow className="fill-gray-900" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pagination.totalItems > 0 && !isLoading && (
|
||||
<div className="pt-4 pb-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={handlePrevious}
|
||||
aria-disabled={pagination.currentPage === 1 || isLoading}
|
||||
className={
|
||||
pagination.currentPage === 1 || isLoading
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "hover:cursor-pointer"
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink isActive>{pagination.currentPage}</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={handleNext}
|
||||
aria-disabled={pagination.currentPage === pagination.totalPages || isLoading}
|
||||
className={
|
||||
pagination.currentPage === pagination.totalPages || isLoading
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "hover:cursor-pointer"
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
59
app/dashboard/uptime/page.tsx
Normal file
59
app/dashboard/uptime/page.tsx
Normal file
@ -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 (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className='inline-block' role='status' aria-label='loading'>
|
||||
<svg className='w-6 h-6 stroke-white animate-spin ' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g clip-path='url(#clip0_9023_61563)'>
|
||||
<path d='M14.6437 2.05426C11.9803 1.2966 9.01686 1.64245 6.50315 3.25548C1.85499 6.23817 0.504864 12.4242 3.48756 17.0724C6.47025 21.7205 12.6563 23.0706 17.3044 20.088C20.4971 18.0393 22.1338 14.4793 21.8792 10.9444' stroke='stroke-current' stroke-width='1.4' stroke-linecap='round' className='my-path'></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id='clip0_9023_61563'>
|
||||
<rect width='24' height='24' fill='white'></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className='sr-only'>Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return isValid ? <Uptime /> : null;
|
||||
}
|
||||
206
app/page.tsx
206
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 (
|
||||
<div className="flex flex-col min-h-screen items-center justify-center gap-6 ">
|
||||
<Card className="w-1/3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your Login data of the compose.yml file below to access
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{errorVisible && (
|
||||
<>
|
||||
<div className="pb-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="mail@example.com"
|
||||
required
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
</div>
|
||||
<Input id="password" type="password" required placeholder="* * * * * * *" onChange={(e) => setPassword(e.target.value)}/>
|
||||
</div>
|
||||
<Button className="w-full" onClick={login}>
|
||||
Login
|
||||
</Button>
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-background to-muted/30 p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="flex justify-center">
|
||||
<div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<KeyRound className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<h1 className="text-4xl font-bold tracking-tight text-foreground">CoreControl</h1>
|
||||
<p className="text-muted-foreground">Sign in to access your dashboard</p>
|
||||
</div>
|
||||
|
||||
<Card className="border-muted/40 shadow-lg">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-semibold">Login</CardTitle>
|
||||
<CardDescription>Enter your credentials to continue</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{errorVisible && (
|
||||
<Alert variant="destructive" className="animate-in fade-in-50 slide-in-from-top-5">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Authentication Error</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
Email
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="mail@example.com"
|
||||
className="pl-10"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password" className="text-sm font-medium">
|
||||
Password
|
||||
</Label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="pl-10"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex flex-col space-y-4">
|
||||
<Button className="w-full" onClick={login} disabled={isLoading}>
|
||||
{isLoading ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
66
components/ui/accordion.tsx
Normal file
66
components/ui/accordion.tsx
Normal file
@ -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<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@ -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
|
||||
|
||||
718
package-lock.json
generated
718
package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
11
prisma/migrations/20250414173201_user_model/migration.sql
Normal file
11
prisma/migrations/20250414173201_user_model/migration.sql
Normal file
@ -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");
|
||||
@ -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")
|
||||
);
|
||||
@ -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
|
||||
}
|
||||
BIN
public/cover.png
Normal file
BIN
public/cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Loading…
x
Reference in New Issue
Block a user