tr]:last:border-b-0", className)}
32 | {...props} />
33 | ))
34 | TableFooter.displayName = "TableFooter"
35 |
36 | const TableRow = React.forwardRef(({ className, ...props }, ref) => (
37 |
44 | ))
45 | TableRow.displayName = "TableRow"
46 |
47 | const TableHead = React.forwardRef(({ className, ...props }, ref) => (
48 | [role=checkbox]]:translate-y-[2px]",
52 | className
53 | )}
54 | {...props} />
55 | ))
56 | TableHead.displayName = "TableHead"
57 |
58 | const TableCell = React.forwardRef(({ className, ...props }, ref) => (
59 | | [role=checkbox]]:translate-y-[2px]",
63 | className
64 | )}
65 | {...props} />
66 | ))
67 | TableCell.displayName = "TableCell"
68 |
69 | const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
70 |
74 | ))
75 | TableCaption.displayName = "TableCaption"
76 |
77 | export {
78 | Table,
79 | TableHeader,
80 | TableBody,
81 | TableFooter,
82 | TableHead,
83 | TableRow,
84 | TableCell,
85 | TableCaption,
86 | }
87 |
--------------------------------------------------------------------------------
/app/src/components/ui/tabs.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "~/lib/utils"
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef(({ className, ...props }, ref) => (
9 |
16 | ))
17 | TabsList.displayName = TabsPrimitive.List.displayName
18 |
19 | const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
20 |
27 | ))
28 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
29 |
30 | const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
31 |
38 | ))
39 | TabsContent.displayName = TabsPrimitive.Content.displayName
40 |
41 | export { Tabs, TabsList, TabsTrigger, TabsContent }
42 |
--------------------------------------------------------------------------------
/app/src/components/ui/textarea.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "~/lib/utils"
4 |
5 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
6 | return (
7 | ()
14 | );
15 | })
16 | Textarea.displayName = "Textarea"
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/app/src/components/ui/toast.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva } from "class-variance-authority";
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
11 |
18 | ))
19 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
20 |
21 | const toastVariants = cva(
22 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
23 | {
24 | variants: {
25 | variant: {
26 | default: "border bg-background text-foreground",
27 | destructive:
28 | "destructive group border-destructive bg-destructive text-destructive-foreground",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | },
34 | }
35 | )
36 |
37 | const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
38 | return (
39 | ()
43 | );
44 | })
45 | Toast.displayName = ToastPrimitives.Root.displayName
46 |
47 | const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
48 |
55 | ))
56 | ToastAction.displayName = ToastPrimitives.Action.displayName
57 |
58 | const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
59 |
67 |
68 |
69 | ))
70 | ToastClose.displayName = ToastPrimitives.Close.displayName
71 |
72 | const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
73 |
77 | ))
78 | ToastTitle.displayName = ToastPrimitives.Title.displayName
79 |
80 | const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
81 |
82 | ))
83 | ToastDescription.displayName = ToastPrimitives.Description.displayName
84 |
85 | export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction };
86 |
--------------------------------------------------------------------------------
/app/src/components/ui/toaster.jsx:
--------------------------------------------------------------------------------
1 | import { useToast } from "~/hooks/use-toast"
2 | import {
3 | Toast,
4 | ToastClose,
5 | ToastDescription,
6 | ToastProvider,
7 | ToastTitle,
8 | ToastViewport,
9 | } from "~/components/ui/toast"
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 | (
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 | (
19 |
20 | {title && {title}}
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 | )
28 | );
29 | })}
30 |
31 | )
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/components/ui/tooltip.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "~/lib/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
13 |
14 |
22 |
23 | ))
24 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
25 |
26 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
27 |
--------------------------------------------------------------------------------
/app/src/hooks/use-mobile.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange);
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/hooks/use-toast.js:
--------------------------------------------------------------------------------
1 | "use client";
2 | // Inspired by react-hot-toast library
3 | import * as React from "react"
4 |
5 | const TOAST_LIMIT = 1
6 | const TOAST_REMOVE_DELAY = 1000000
7 |
8 | const actionTypes = {
9 | ADD_TOAST: "ADD_TOAST",
10 | UPDATE_TOAST: "UPDATE_TOAST",
11 | DISMISS_TOAST: "DISMISS_TOAST",
12 | REMOVE_TOAST: "REMOVE_TOAST"
13 | }
14 |
15 | let count = 0
16 |
17 | function genId() {
18 | count = (count + 1) % Number.MAX_SAFE_INTEGER
19 | return count.toString();
20 | }
21 |
22 | const toastTimeouts = new Map()
23 |
24 | const addToRemoveQueue = (toastId) => {
25 | if (toastTimeouts.has(toastId)) {
26 | return
27 | }
28 |
29 | const timeout = setTimeout(() => {
30 | toastTimeouts.delete(toastId)
31 | dispatch({
32 | type: "REMOVE_TOAST",
33 | toastId: toastId,
34 | })
35 | }, TOAST_REMOVE_DELAY)
36 |
37 | toastTimeouts.set(toastId, timeout)
38 | }
39 |
40 | export const reducer = (state, action) => {
41 | switch (action.type) {
42 | case "ADD_TOAST":
43 | return {
44 | ...state,
45 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
46 | };
47 |
48 | case "UPDATE_TOAST":
49 | return {
50 | ...state,
51 | toasts: state.toasts.map((t) =>
52 | t.id === action.toast.id ? { ...t, ...action.toast } : t),
53 | };
54 |
55 | case "DISMISS_TOAST": {
56 | const { toastId } = action
57 |
58 | // ! Side effects ! - This could be extracted into a dismissToast() action,
59 | // but I'll keep it here for simplicity
60 | if (toastId) {
61 | addToRemoveQueue(toastId)
62 | } else {
63 | state.toasts.forEach((toast) => {
64 | addToRemoveQueue(toast.id)
65 | })
66 | }
67 |
68 | return {
69 | ...state,
70 | toasts: state.toasts.map((t) =>
71 | t.id === toastId || toastId === undefined
72 | ? {
73 | ...t,
74 | open: false,
75 | }
76 | : t),
77 | };
78 | }
79 | case "REMOVE_TOAST":
80 | if (action.toastId === undefined) {
81 | return {
82 | ...state,
83 | toasts: [],
84 | }
85 | }
86 | return {
87 | ...state,
88 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
89 | };
90 | }
91 | }
92 |
93 | const listeners = []
94 |
95 | let memoryState = { toasts: [] }
96 |
97 | function dispatch(action) {
98 | memoryState = reducer(memoryState, action)
99 | listeners.forEach((listener) => {
100 | listener(memoryState)
101 | })
102 | }
103 |
104 | function toast({
105 | ...props
106 | }) {
107 | const id = genId()
108 |
109 | const update = (props) =>
110 | dispatch({
111 | type: "UPDATE_TOAST",
112 | toast: { ...props, id },
113 | })
114 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
115 |
116 | dispatch({
117 | type: "ADD_TOAST",
118 | toast: {
119 | ...props,
120 | id,
121 | open: true,
122 | onOpenChange: (open) => {
123 | if (!open) dismiss()
124 | },
125 | },
126 | })
127 |
128 | return {
129 | id: id,
130 | dismiss,
131 | update,
132 | }
133 | }
134 |
135 | function useToast() {
136 | const [state, setState] = React.useState(memoryState)
137 |
138 | React.useEffect(() => {
139 | listeners.push(setState)
140 | return () => {
141 | const index = listeners.indexOf(setState)
142 | if (index > -1) {
143 | listeners.splice(index, 1)
144 | }
145 | };
146 | }, [state])
147 |
148 | return {
149 | ...state,
150 | toast,
151 | dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
152 | };
153 | }
154 |
155 | export { useToast, toast }
156 |
--------------------------------------------------------------------------------
/app/src/index.css:
--------------------------------------------------------------------------------
1 |
2 | @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap');
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | .font-mono {
8 | font-family: 'JetBrains Mono', monospace;
9 | }
10 |
11 | @font-face {
12 | font-family: 'Mozilla Text';
13 | src: url('/assets/MozillaText-Variable.woff2') format('woff2-variations'),
14 | url('/assets/MozillaText-Variable.woff') format('woff-variations'),
15 | url('/assets/MozillaText-Variable.ttf') format('truetype-variations');
16 | font-weight: 100 900;
17 | font-style: normal;
18 | }
19 |
20 | @font-face {
21 | font-family: 'Mozilla Text';
22 | src: url('/assets/MozillaTextItalic-Variable.woff2') format('woff2-variations'),
23 | url('/assets/MozillaTextItalic-Variable.woff') format('woff-variations'),
24 | url('/assets/MozillaTextItalic-Variable.ttf') format('truetype-variations');
25 | font-weight: 100 900;
26 | font-style: italic;
27 | }
28 |
29 | * {
30 | font-family: 'Mozilla Text', sans-serif;
31 | }
32 |
33 | @layer base {
34 | :root {
35 | --background: 0 0% 100%;
36 | --foreground: 0 0% 3.9%;
37 | --card: 0 0% 100%;
38 | --card-foreground: 0 0% 3.9%;
39 | --popover: 0 0% 100%;
40 | --popover-foreground: 0 0% 3.9%;
41 | --primary: 0 0% 9%;
42 | --primary-foreground: 0 0% 98%;
43 | --secondary: 0 0% 96.1%;
44 | --secondary-foreground: 0 0% 9%;
45 | --muted: 0 0% 96.1%;
46 | --muted-foreground: 0 0% 45.1%;
47 | --accent: 0 0% 96.1%;
48 | --accent-foreground: 0 0% 9%;
49 | --destructive: 0 84.2% 60.2%;
50 | --destructive-foreground: 0 0% 98%;
51 | --border: 0 0% 89.8%;
52 | --input: 0 0% 89.8%;
53 | --ring: 0 0% 3.9%;
54 | --chart-1: 12 76% 61%;
55 | --chart-2: 173 58% 39%;
56 | --chart-3: 197 37% 24%;
57 | --chart-4: 43 74% 66%;
58 | --chart-5: 27 87% 67%;
59 | --radius: 0.5rem;
60 | --sidebar-background: 0 0% 98%;
61 | --sidebar-foreground: 240 5.3% 26.1%;
62 | --sidebar-primary: 240 5.9% 10%;
63 | --sidebar-primary-foreground: 0 0% 98%;
64 | --sidebar-accent: 240 4.8% 95.9%;
65 | --sidebar-accent-foreground: 240 5.9% 10%;
66 | --sidebar-border: 220 13% 91%;
67 | --sidebar-ring: 217.2 91.2% 59.8%;
68 | }
69 | .dark {
70 | --background: 0 0% 3.9%;
71 | --foreground: 0 0% 98%;
72 | --card: 0 0% 3.9%;
73 | --card-foreground: 0 0% 98%;
74 | --popover: 0 0% 3.9%;
75 | --popover-foreground: 0 0% 98%;
76 | --primary: 0 0% 98%;
77 | --primary-foreground: 0 0% 9%;
78 | --secondary: 0 0% 14.9%;
79 | --secondary-foreground: 0 0% 98%;
80 | --muted: 0 0% 14.9%;
81 | --muted-foreground: 0 0% 63.9%;
82 | --accent: 0 0% 14.9%;
83 | --accent-foreground: 0 0% 98%;
84 | --destructive: 0 62.8% 30.6%;
85 | --destructive-foreground: 0 0% 98%;
86 | --border: 0 0% 14.9%;
87 | --input: 0 0% 14.9%;
88 | --ring: 0 0% 83.1%;
89 | --chart-1: 220 70% 50%;
90 | --chart-2: 160 60% 45%;
91 | --chart-3: 30 80% 55%;
92 | --chart-4: 280 65% 60%;
93 | --chart-5: 340 75% 55%;
94 | --sidebar-background: 240 5.9% 10%;
95 | --sidebar-foreground: 240 4.8% 95.9%;
96 | --sidebar-primary: 224.3 76.3% 48%;
97 | --sidebar-primary-foreground: 0 0% 100%;
98 | --sidebar-accent: 240 3.7% 15.9%;
99 | --sidebar-accent-foreground: 240 4.8% 95.9%;
100 | --sidebar-border: 240 3.7% 15.9%;
101 | --sidebar-ring: 217.2 91.2% 59.8%;
102 | }
103 | }
104 |
105 | @layer base {
106 | * {
107 | @apply border-border;
108 | }
109 | body {
110 | @apply bg-background text-foreground;
111 | }
112 | }
--------------------------------------------------------------------------------
/app/src/lib/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export const api = axios.create({
4 | baseURL: '/api',
5 | withCredentials: true
6 | })
--------------------------------------------------------------------------------
/app/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main.jsx:
--------------------------------------------------------------------------------
1 | {/**
2 |
3 | Mono v0.1.0 for Prism - codename "Adelante"
4 | © 2024 Matt James, MJI Inc and contributors
5 |
6 | */}
7 |
8 | import React from 'react'
9 | import ReactDOM from 'react-dom/client'
10 | import { BrowserRouter } from 'react-router-dom'
11 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
12 | import App from './App'
13 | import './index.css'
14 |
15 | const queryClient = new QueryClient()
16 |
17 | ReactDOM.createRoot(document.getElementById('MonoApp')).render(
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
--------------------------------------------------------------------------------
/app/src/pages/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '@/components/ui/button';
3 | import { Card, CardContent } from '@/components/ui/card';
4 | import { useNavigate } from 'react-router-dom';
5 | import { Home, AlertCircle } from 'lucide-react';
6 |
7 | const NotFound = () => {
8 | const navigate = useNavigate();
9 |
10 | return (
11 |
12 |
13 |
14 |
15 | {/* Animated Text */}
16 |
17 |
18 | 404
19 |
20 |
21 |
22 | Oops! This page seems to be missing.
23 |
24 |
25 | Prism couldn't find the page you were looking for.
26 |
27 |
28 |
29 | {/* Action Buttons */}
30 |
31 |
38 |
45 |
46 |
47 |
48 |
49 |
50 |
92 |
93 | );
94 | };
95 |
96 | export default NotFound;
--------------------------------------------------------------------------------
/app/src/pages/Referrals.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Gift, HelpCircle, Copy } from 'lucide-react';
3 | import { Button } from "@/components/ui/button";
4 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5 | import { Input } from "@/components/ui/input";
6 | import { Alert, AlertDescription } from "@/components/ui/alert";
7 |
8 | const ReferralsPage = () => {
9 | const [newCode, setNewCode] = useState('');
10 | const [message, setMessage] = useState(null);
11 | const [isSubmitting, setIsSubmitting] = useState(false);
12 |
13 | const handleGenerateCode = async () => {
14 | if (!newCode) {
15 | setMessage({ type: 'error', text: 'Please enter a code' });
16 | return;
17 | }
18 |
19 | setIsSubmitting(true);
20 | try {
21 | const response = await fetch(`/generate?code=${encodeURIComponent(newCode)}`);
22 | const data = await response.json();
23 |
24 | if (data.error) {
25 | setMessage({ type: 'error', text: data.error });
26 | } else {
27 | setMessage({ type: 'success', text: 'Successfully created referral code!' });
28 | setNewCode('');
29 | }
30 | } catch (error) {
31 | setMessage({ type: 'error', text: 'Failed to generate code. Please try again.' });
32 | }
33 | setIsSubmitting(false);
34 | };
35 |
36 | const copyToClipboard = (text) => {
37 | navigator.clipboard.writeText(text);
38 | setMessage({ type: 'success', text: 'Copied to clipboard!' });
39 | };
40 |
41 | return (
42 |
43 | {/* How Referrals Work */}
44 |
45 |
46 |
47 |
48 | Information
49 |
50 |
51 |
52 |
53 |
54 |
55 | Rewards
56 |
57 |
58 | - • When someone uses your code, you get 80 coins
59 | - • They receive 250 coins for using a referral code
60 | - • Each user can only claim one referral code
61 | - • You cannot claim your own referral code
62 |
63 |
64 |
65 |
66 |
67 |
68 | {/* Generate Code Section */}
69 |
70 |
71 | Generate Referral Code
72 | Create a unique code for others to use
73 |
74 |
75 |
76 | setNewCode(e.target.value)}
80 | maxLength={15}
81 | className="bg-neutral-800/50"
82 | />
83 |
89 |
90 |
91 | {message && (
92 |
93 | {message.text}
94 |
95 | )}
96 |
97 |
98 |
99 | );
100 | };
101 |
102 | export default ReferralsPage;
--------------------------------------------------------------------------------
/app/src/pages/coins/AFKPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
3 | import { Progress } from '@/components/ui/progress';
4 | import { Badge } from '@/components/ui/badge';
5 | import { Coins, Clock, History, AlertCircle } from 'lucide-react';
6 | import { Alert, AlertDescription } from '@/components/ui/alert';
7 |
8 | export default function AFKPage() {
9 | const [connected, setConnected] = useState(false);
10 | const [nextReward, setNextReward] = useState(60000);
11 | const [coinsPerMinute, setCoinsPerMinute] = useState(1.5);
12 | const [totalEarned, setTotalEarned] = useState(0);
13 | const [sessionTime, setSessionTime] = useState(0);
14 | const [error, setError] = useState('');
15 |
16 | useEffect(() => {
17 | const ws = new WebSocket('/ws');
18 |
19 | ws.onopen = () => {
20 | setConnected(true);
21 | setError('');
22 | };
23 |
24 | ws.onmessage = (event) => {
25 | const data = JSON.parse(event.data);
26 | if (data.type === 'afk_state') {
27 | setNextReward(data.nextRewardIn);
28 | setCoinsPerMinute(data.coinsPerMinute);
29 | setTotalEarned(prev => prev + (data.nextRewardIn === 0 ? data.coinsPerMinute : 0));
30 | }
31 | };
32 |
33 | ws.onclose = (event) => {
34 | setConnected(false);
35 | if (event.code === 4001) {
36 | setError('You must be logged in to earn AFK rewards');
37 | } else if (event.code === 4002) {
38 | setError('AFK rewards are already running in another tab');
39 | } else {
40 | setError('Connection lost. Please refresh the page.');
41 | }
42 | };
43 |
44 | // Track session time
45 | const interval = setInterval(() => {
46 | setSessionTime(prev => prev + 1);
47 | }, 1000);
48 |
49 | return () => {
50 | ws.close();
51 | clearInterval(interval);
52 | };
53 | }, []);
54 |
55 | const formatTime = (seconds) => {
56 | const hrs = Math.floor(seconds / 3600);
57 | const mins = Math.floor((seconds % 3600) / 60);
58 | const secs = seconds % 60;
59 | return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
60 | };
61 |
62 | return (
63 |
64 |
65 | AFK page
66 |
67 | {connected ? 'CONNECTED' : 'DISCONNECTED'}
68 |
69 |
70 |
71 | {error && (
72 |
73 |
74 | {error}
75 |
76 | )}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | Earnings Rate
86 |
87 |
88 |
89 |
90 | {coinsPerMinute.toFixed(1)} coins/min
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Session Time
102 |
103 |
104 |
105 |
106 | {formatTime(sessionTime)}
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | Next Reward
115 |
116 |
117 |
118 |
119 |
120 |
121 | Next reward in {Math.ceil(nextReward / 1000)} seconds
122 |
123 |
124 |
125 |
126 |
127 |
128 | How it works
129 |
130 |
131 |
132 | Earn coins automatically just by keeping this page open! You'll receive {coinsPerMinute} coins every minute.
133 |
134 |
135 | You can use these coins to purchase resources and upgrades in the store.
136 |
137 |
138 |
139 |
140 | );
141 | }
--------------------------------------------------------------------------------
/app/src/pages/server/UserManagerPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import axios from 'axios';
4 | import { Button } from "@/components/ui/button";
5 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6 | import { ScrollArea } from "@/components/ui/scroll-area";
7 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
8 | import { Trash2, Plus, RefreshCw } from 'lucide-react';
9 | import { Input } from "@/components/ui/input";
10 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
11 |
12 | const UsersPage = () => {
13 | const { id } = useParams();
14 | const [users, setUsers] = useState([]);
15 | const [loading, setLoading] = useState(false);
16 | const [error, setError] = useState(null);
17 | const [isAddModalOpen, setIsAddModalOpen] = useState(false);
18 | const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
19 | const [selectedUser, setSelectedUser] = useState(null);
20 | const [newUserEmail, setNewUserEmail] = useState('');
21 |
22 | const syncSubuserServers = async () => {
23 | try {
24 | await axios.post('/api/subuser-servers-sync');
25 | } catch (err) {
26 | console.error('Failed to sync subuser servers:', err);
27 | }
28 | };
29 |
30 | const fetchUsers = async () => {
31 | setLoading(true);
32 | setError(null);
33 | try {
34 | const response = await axios.get(`/api/server/${id}/users`);
35 | setUsers(response.data.data);
36 | await syncSubuserServers();
37 | } catch (err) {
38 | setError('Failed to fetch users. Please try again later.');
39 | console.error(err);
40 | } finally {
41 | setLoading(false);
42 | }
43 | };
44 |
45 | const handleAddUser = async () => {
46 | try {
47 | const response = await axios.post(`/api/server/${id}/users`, { email: newUserEmail });
48 | setUsers([...users, response.data]);
49 | setIsAddModalOpen(false);
50 | setNewUserEmail('');
51 | await syncSubuserServers();
52 | } catch (err) {
53 | setError('Failed to add user. Please try again later.');
54 | console.error(err);
55 | }
56 | };
57 |
58 | const handleDeleteUser = async () => {
59 | try {
60 | await axios.delete(`/api/server/${id}/users/${selectedUser.attributes.uuid}`);
61 | setUsers(users.filter(user => user.attributes.uuid !== selectedUser.attributes.uuid));
62 | setIsDeleteModalOpen(false);
63 | await syncSubuserServers();
64 | } catch (err) {
65 | setError('Failed to delete user. Please try again later.');
66 | console.error(err);
67 | }
68 | };
69 |
70 | useEffect(() => {
71 | fetchUsers();
72 | }, [id]);
73 |
74 | return (
75 |
76 |
77 | Users
78 |
82 |
83 |
84 |
85 |
86 | Sub-users
87 |
88 |
89 | {loading ? (
90 |
91 |
92 |
93 | ) : error ? (
94 |
95 | {error}
96 |
97 | ) : (
98 |
99 |
100 |
101 |
102 | Username
103 | Email
104 | Actions
105 |
106 |
107 |
108 | {users.map((user) => (
109 |
110 | {user.attributes.username}
111 | {user.attributes.email}
112 |
113 |
124 |
125 |
126 | ))}
127 |
128 |
129 |
130 | )}
131 |
132 |
133 |
134 | {/* Add User Modal */}
135 |
158 |
159 | {/* Delete User Modal */}
160 |
179 |
180 | );
181 | };
182 |
183 | export default UsersPage;
--------------------------------------------------------------------------------
/app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | darkMode: ["class"],
4 | content: [
5 | "./index.html",
6 | "./src/**/*.{js,ts,jsx,tsx}",
7 | ],
8 | theme: {
9 | extend: {
10 | borderRadius: {
11 | lg: 'var(--radius)',
12 | md: 'calc(var(--radius) - 2px)',
13 | sm: 'calc(var(--radius) - 4px)'
14 | },
15 | colors: {
16 | background: 'hsl(var(--background))',
17 | foreground: 'hsl(var(--foreground))',
18 | card: {
19 | DEFAULT: 'hsl(var(--card))',
20 | foreground: 'hsl(var(--card-foreground))'
21 | },
22 | popover: {
23 | DEFAULT: 'hsl(var(--popover))',
24 | foreground: 'hsl(var(--popover-foreground))'
25 | },
26 | primary: {
27 | DEFAULT: 'hsl(var(--primary))',
28 | foreground: 'hsl(var(--primary-foreground))'
29 | },
30 | secondary: {
31 | DEFAULT: 'hsl(var(--secondary))',
32 | foreground: 'hsl(var(--secondary-foreground))'
33 | },
34 | muted: {
35 | DEFAULT: 'hsl(var(--muted))',
36 | foreground: 'hsl(var(--muted-foreground))'
37 | },
38 | accent: {
39 | DEFAULT: 'hsl(var(--accent))',
40 | foreground: 'hsl(var(--accent-foreground))'
41 | },
42 | destructive: {
43 | DEFAULT: 'hsl(var(--destructive))',
44 | foreground: 'hsl(var(--destructive-foreground))'
45 | },
46 | border: 'hsl(var(--border))',
47 | input: 'hsl(var(--input))',
48 | ring: 'hsl(var(--ring))',
49 | chart: {
50 | '1': 'hsl(var(--chart-1))',
51 | '2': 'hsl(var(--chart-2))',
52 | '3': 'hsl(var(--chart-3))',
53 | '4': 'hsl(var(--chart-4))',
54 | '5': 'hsl(var(--chart-5))'
55 | },
56 | sidebar: {
57 | DEFAULT: 'hsl(var(--sidebar-background))',
58 | foreground: 'hsl(var(--sidebar-foreground))',
59 | primary: 'hsl(var(--sidebar-primary))',
60 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
61 | accent: 'hsl(var(--sidebar-accent))',
62 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
63 | border: 'hsl(var(--sidebar-border))',
64 | ring: 'hsl(var(--sidebar-ring))'
65 | }
66 | },
67 | keyframes: {
68 | 'accordion-down': {
69 | from: {
70 | height: '0'
71 | },
72 | to: {
73 | height: 'var(--radix-accordion-content-height)'
74 | }
75 | },
76 | 'accordion-up': {
77 | from: {
78 | height: 'var(--radix-accordion-content-height)'
79 | },
80 | to: {
81 | height: '0'
82 | }
83 | }
84 | },
85 | animation: {
86 | 'accordion-down': 'accordion-down 0.2s ease-out',
87 | 'accordion-up': 'accordion-up 0.2s ease-out'
88 | }
89 | }
90 | },
91 | plugins: [require("tailwindcss-animate")],
92 | }
--------------------------------------------------------------------------------
/app/vite.config.js:
--------------------------------------------------------------------------------
1 | // vite.config.js
2 | import { defineConfig } from 'vite'
3 | import react from '@vitejs/plugin-react'
4 | import path from 'path'
5 |
6 | export default defineConfig({
7 | assetsInclude: [
8 | '**/*.woff2',
9 | '**/*.woff',
10 | '**/*.ttf'
11 | ],
12 | chunkSizeWarningLimit: 1000,
13 | plugins: [react()],
14 | base: '/app',
15 | build: {
16 | commonjsOptions: {
17 | onwarn: () => {}
18 | },
19 | outDir: 'dist',
20 | assetsDir: 'assets',
21 | // Generate manifest for asset tracking
22 | manifest: true,
23 | rollupOptions: {
24 | onwarn(warning, warn) {
25 | if (warning) return;
26 | },
27 | input: path.resolve(__dirname, 'index.html'),
28 | output: {
29 | // Ensure assets use panel prefix
30 | assetFileNames: (assetInfo) => {
31 | const info = assetInfo.name.split('.')
32 | const ext = info[info.length - 1]
33 | return `assets/${ext}/[name]-[hash][extname]`
34 | },
35 | chunkFileNames: 'assets/js/[name]-[hash].js',
36 | entryFileNames: 'assets/js/[name]-[hash].js',
37 | }
38 | },
39 | },
40 | resolve: {
41 | alias: {
42 | "~": path.resolve(__dirname, "./src"),
43 | '@': path.resolve(__dirname, './src'),
44 | '@components': path.resolve(__dirname, './src/components'),
45 | '@layouts': path.resolve(__dirname, './src/layouts'),
46 | '@pages': path.resolve(__dirname, './src/pages'),
47 | '@lib': path.resolve(__dirname, './src/lib')
48 | }
49 | }
50 | })
--------------------------------------------------------------------------------
/assets/MozillaHeadline-Variable.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaHeadline-Variable.ttf
--------------------------------------------------------------------------------
/assets/MozillaHeadline-Variable.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaHeadline-Variable.woff
--------------------------------------------------------------------------------
/assets/MozillaHeadline-Variable.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaHeadline-Variable.woff2
--------------------------------------------------------------------------------
/assets/MozillaHeadlineItalic-Variable.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaHeadlineItalic-Variable.ttf
--------------------------------------------------------------------------------
/assets/MozillaHeadlineItalic-Variable.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaHeadlineItalic-Variable.woff
--------------------------------------------------------------------------------
/assets/MozillaHeadlineItalic-Variable.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaHeadlineItalic-Variable.woff2
--------------------------------------------------------------------------------
/assets/MozillaText-Variable.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaText-Variable.ttf
--------------------------------------------------------------------------------
/assets/MozillaText-Variable.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaText-Variable.woff
--------------------------------------------------------------------------------
/assets/MozillaText-Variable.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaText-Variable.woff2
--------------------------------------------------------------------------------
/assets/MozillaTextItalic-Variable.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaTextItalic-Variable.ttf
--------------------------------------------------------------------------------
/assets/MozillaTextItalic-Variable.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaTextItalic-Variable.woff
--------------------------------------------------------------------------------
/assets/MozillaTextItalic-Variable.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrismFOSS/Prism/823d1d2a1fa5e8d9625cf1b8753d766bf402fad7/assets/MozillaTextItalic-Variable.woff2
--------------------------------------------------------------------------------
/assets/tw.conf:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/example_config.toml:
--------------------------------------------------------------------------------
1 | # ═════════════════════════════════════════════════════════
2 | # Prism Config
3 | # This config will work with Prism 0.5.x
4 | # or any Adelante based distribution. (120+)
5 | # ═════════════════════════════════════════════════════════
6 |
7 | # ---------------------------------
8 | # General Configuration
9 | # ---------------------------------
10 | name = "Prism" # Website/dashboard name
11 | auto_update = false # Automatically update Prism on boot
12 |
13 | # ---------------------------------
14 | # Heliactyl Release Information
15 | # ---------------------------------
16 | platform_codename = "Adelante"
17 | platform_level = 130 # channel: stable - 10-01-2025 (Adelante R)
18 | version = "0.5.0"
19 |
20 | # ---------------------------------
21 | # Additional Settings
22 | # ---------------------------------
23 | database = "prism.db"
24 |
25 | # ---------------------------------
26 | # Pterodactyl Settings
27 | # ---------------------------------
28 | [pterodactyl]
29 | domain = "https://panel.example.com" # Format: https://panel.domain.tld with no / on the end
30 | key = "ptla_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Find this under Admin -> Application API
31 | client_key = "ptlc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Create an admin account and generate from Account -> API Keys
32 |
33 | # ---------------------------------
34 | # Webserver Configuration
35 | # ---------------------------------
36 | [website]
37 | port = 25_001 # Default: 25_001
38 | secret = "CHANGE_THIS_SECRET" # Change this - do not run Prism in production with the default secret
39 | domain = "https://example.com"
40 |
41 | # ---------------------------------
42 | # API Client Settings
43 | # ---------------------------------
44 | [api.client]
45 | accountSwitcher = true
46 |
47 | [api.client.resend]
48 | # ---------------------------------
49 | # Resend API Key
50 | #
51 | # This is required, but no worries as Resend is free.
52 | # ---------------------------------
53 | api_key = "re_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx"
54 | from = "noreply@example.com"
55 | app_name = "Prism"
56 |
57 | [api.client.api] # Prism API version 5
58 | enabled = true
59 | code = "YOUR_API_KEY"
60 |
61 | [api.client.passwordgenerator]
62 | signup = true # Disable user signups
63 | length = 24
64 |
65 | [api.client.allow]
66 | regen = true # Allow users to regenerate their SFTP/panel password
67 | overresourcessuspend = false # Suspend users if they go over resource limits
68 |
69 | [api.client.allow.server]
70 | create = true
71 | modify = true
72 | delete = true
73 |
74 | [api.client.packages]
75 | default = "default" # Default package assigned to users
76 |
77 | # ---------------------------------
78 | # Package Configuration
79 | # ---------------------------------
80 | [api.client.packages.list.default]
81 | ram = 4_096
82 | disk = 20_480
83 | cpu = 150
84 | servers = 2
85 |
86 | # -----------------------------------------------------
87 | # Location Configuration
88 | # -----------------------------------------------------
89 | [api.client.locations.1]
90 | name = "Example Location"
91 |
92 | # ---------------------------------
93 | # Egg Configuration
94 | # ---------------------------------
95 |
96 | # Paper
97 | [api.client.eggs.paper]
98 | display = "Minecraft: Java Edition - Paper"
99 |
100 | [api.client.eggs.paper.minimum]
101 | ram = 1_024
102 | disk = 1_024
103 | cpu = 80
104 |
105 | [api.client.eggs.paper.info]
106 | egg = 2
107 | docker_image = "ghcr.io/pterodactyl/yolks:java_21"
108 | startup = "java -Xms128M -Xmx{{SERVER_MEMORY}}M -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}"
109 |
110 | [api.client.eggs.paper.info.environment]
111 | SERVER_JARFILE = "server.jar"
112 | BUILD_NUMBER = "latest"
113 |
114 | [api.client.eggs.paper.info.feature_limits]
115 | databases = 4
116 | backups = 4
117 |
118 | # Add more eggs the same way as Paper above
119 |
120 | # ---------------------------------
121 | # Coin Configuration
122 | # ---------------------------------
123 | [api.client.coins]
124 | enabled = true
125 |
126 | [api.client.coins.store]
127 | enabled = true
128 |
129 | [api.client.coins.store.ram]
130 | cost = 775
131 |
132 | [api.client.coins.store.disk]
133 | cost = 400
134 |
135 | [api.client.coins.store.cpu]
136 | cost = 650
137 |
138 | [api.client.coins.store.servers]
139 | cost = 300
140 |
141 | # ---------------------------------
142 | # Logging Configuration
143 | # ---------------------------------
144 | [logging]
145 | status = true
146 | webhook = "https://discord.com/api/webhooks/your_webhook_url"
147 |
148 | [logging.actions.user]
149 | signup = true
150 | "create server" = true
151 | "gifted coins" = true
152 | "modify server" = true
153 | "buy servers" = true
154 | "buy ram" = true
155 | "buy cpu" = true
156 | "buy disk" = true
157 |
158 | [logging.actions.admin]
159 | "set coins" = true
160 | "add coins" = true
161 | "set resources" = true
162 | "set plan" = true
163 | "create coupon" = true
164 | "revoke coupon" = true
165 | "remove account" = true
166 | "view ip" = true
--------------------------------------------------------------------------------
/handlers/Client.js:
--------------------------------------------------------------------------------
1 | const WebSocket = require('ws');
2 | const axios = require('axios');
3 |
4 | class PterodactylClientModule {
5 | constructor(apiUrl, apiKey) {
6 | this.apiUrl = apiUrl;
7 | this.apiKey = apiKey;
8 | this.socket = null;
9 | this.token = null;
10 | this.serverId = null;
11 | this.eventHandlers = {};
12 | }
13 |
14 | async getServerDetails(serverId, includeEgg = false, includeSubusers = false) {
15 | try {
16 | const response = await axios.get(`${this.apiUrl}/api/client/servers/${serverId}`, {
17 | headers: {
18 | 'Accept': 'application/json',
19 | 'Content-Type': 'application/json',
20 | 'Authorization': `Bearer ${this.apiKey}`
21 | },
22 | params: {
23 | include: [
24 | ...(includeEgg ? ['egg'] : []),
25 | ...(includeSubusers ? ['subusers'] : [])
26 | ].join(',')
27 | }
28 | });
29 | return response.data;
30 | } catch (error) {
31 | console.error('Error fetching server details:', error);
32 | throw error;
33 | }
34 | }
35 |
36 | async connectWebSocket(serverId) {
37 | try {
38 | const response = await axios.get(`${this.apiUrl}/api/client/servers/${serverId}/websocket`, {
39 | headers: {
40 | 'Accept': 'application/json',
41 | 'Content-Type': 'application/json',
42 | 'Authorization': `Bearer ${this.apiKey}`
43 | }
44 | });
45 |
46 | const { token, socket: socketUrl } = response.data.data;
47 | this.token = token;
48 | this.serverId = serverId;
49 |
50 | this.socket = new WebSocket(socketUrl);
51 |
52 | this.socket.on('open', () => {
53 | console.log('WebSocket connected');
54 | this.authenticate();
55 | });
56 |
57 | this.socket.on('message', (data) => {
58 | const message = JSON.parse(data);
59 | this.handleWebSocketMessage(message);
60 | });
61 |
62 | this.socket.on('close', () => {
63 | console.log('WebSocket disconnected');
64 | });
65 |
66 | this.socket.on('error', (error) => {
67 | console.error('WebSocket error:', error);
68 | });
69 | } catch (error) {
70 | console.error('Error connecting to WebSocket:', error);
71 | throw error;
72 | }
73 | }
74 |
75 | authenticate() {
76 | this.sendToWebSocket('auth', [this.token]);
77 | }
78 |
79 | sendToWebSocket(event, args) {
80 | if (this.socket && this.socket.readyState === WebSocket.OPEN) {
81 | this.socket.send(JSON.stringify({ event, args }));
82 | } else {
83 | console.error('WebSocket is not connected');
84 | }
85 | }
86 |
87 | handleWebSocketMessage(message) {
88 | switch (message.event) {
89 | case 'auth success':
90 | console.log('Authentication successful');
91 | this.sendToWebSocket('send logs', [null]);
92 | break;
93 | case 'token expiring':
94 | console.log('Token is expiring soon, requesting a new one');
95 | this.refreshToken();
96 | break;
97 | case 'token expired':
98 | console.log('Token has expired, requesting a new one');
99 | this.refreshToken();
100 | break;
101 | case 'status':
102 | case 'console output':
103 | case 'stats':
104 | if (this.eventHandlers[message.event]) {
105 | this.eventHandlers[message.event](message.args);
106 | }
107 | break;
108 | default:
109 | console.log('Unhandled WebSocket message:', message);
110 | }
111 | }
112 |
113 | async refreshToken() {
114 | try {
115 | const response = await axios.get(`${this.apiUrl}/api/client/servers/${this.serverId}/websocket`, {
116 | headers: {
117 | 'Accept': 'application/json',
118 | 'Content-Type': 'application/json',
119 | 'Authorization': `Bearer ${this.apiKey}`
120 | }
121 | });
122 |
123 | this.token = response.data.data.token;
124 | this.authenticate();
125 | } catch (error) {
126 | console.error('Error refreshing token:', error);
127 | }
128 | }
129 |
130 | on(event, callback) {
131 | this.eventHandlers[event] = callback;
132 | }
133 |
134 | requestStats() {
135 | this.sendToWebSocket('send stats', [null]);
136 | }
137 |
138 | sendCommand(command) {
139 | this.sendToWebSocket('send command', [command]);
140 | }
141 |
142 | setPowerState(state) {
143 | this.sendToWebSocket('set state', [state]);
144 | }
145 |
146 | disconnect() {
147 | if (this.socket) {
148 | this.socket.close();
149 | }
150 | }
151 | }
152 |
153 | module.exports = PterodactylClientModule;
--------------------------------------------------------------------------------
/handlers/Queue.js:
--------------------------------------------------------------------------------
1 | class Queue {
2 | constructor() {
3 | this.queue = []
4 | this.processing = false;
5 | }
6 |
7 | addJob(job) {
8 | this.queue.push(job)
9 | this.bumpQueue()
10 | }
11 |
12 | bumpQueue() {
13 | if (this.processing) return
14 | const job = this.queue.shift()
15 | if (!job) return
16 | const cb = () => {
17 | this.processing = false
18 | this.bumpQueue()
19 | }
20 | this.processing = true
21 | job(cb)
22 | }
23 | }
24 |
25 | module.exports = Queue
--------------------------------------------------------------------------------
/handlers/afk.js:
--------------------------------------------------------------------------------
1 | module.exports = `
2 | const x = 'hi';
3 | `;
4 |
--------------------------------------------------------------------------------
/handlers/config.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const toml = require('@iarna/toml');
3 | const path = require('path');
4 |
5 | let configCache = {};
6 | let watchers = new Map();
7 |
8 | /**
9 | * Loads and parses a TOML file and returns it as a JSON object.
10 | * Implements caching and robust file watching for automatic updates.
11 | *
12 | * @param {string} filePath - The path to the TOML file.
13 | * @returns {object} - The parsed TOML content as a JSON object.
14 | */
15 | function loadConfig(filePath = 'config.toml') {
16 | // Resolve full path
17 | const fullPath = path.resolve(filePath);
18 |
19 | try {
20 | // Check if config is already cached
21 | if (!configCache[fullPath]) {
22 | // Initial load
23 | const tomlString = fs.readFileSync(fullPath, 'utf8');
24 | configCache[fullPath] = toml.parse(tomlString);
25 |
26 | // Set up file watcher if not already watching
27 | if (!watchers.has(fullPath)) {
28 | const watcher = fs.watch(fullPath, {persistent: true}, (eventType) => {
29 | if (eventType === 'change') {
30 | // Add small delay to ensure file is fully written
31 | setTimeout(() => {
32 | try {
33 | const updatedTomlString = fs.readFileSync(fullPath, 'utf8');
34 | const updatedConfig = toml.parse(updatedTomlString);
35 | configCache[fullPath] = updatedConfig;
36 | console.log(`Configuration updated: ${fullPath}`);
37 | } catch (watcherErr) {
38 | console.error(`Error updating configuration ${fullPath}:`, watcherErr);
39 | // Keep using existing config on error
40 | }
41 | }, 100);
42 | }
43 | });
44 |
45 | // Handle watcher errors
46 | watcher.on('error', (error) => {
47 | console.error(`Watcher error for ${fullPath}:`, error);
48 | watcher.close();
49 | watchers.delete(fullPath);
50 | });
51 |
52 | watchers.set(fullPath, watcher);
53 | }
54 | }
55 |
56 | return configCache[fullPath];
57 | } catch (err) {
58 | console.error(`Error reading or parsing TOML file ${fullPath}:`, err);
59 | throw err;
60 | }
61 | }
62 |
63 | // Clean up watchers on process exit
64 | process.on('exit', () => {
65 | for (const watcher of watchers.values()) {
66 | watcher.close();
67 | }
68 | watchers.clear();
69 | });
70 |
71 | module.exports = loadConfig;
--------------------------------------------------------------------------------
/handlers/console.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Configures and initializes a structured logging system using Winston
3 | * @module handlers/console
4 | */
5 |
6 | const winston = require('winston');
7 | const { format, transports } = winston;
8 |
9 | /**
10 | * Creates and configures a production-ready Winston logger with standardized formatting
11 | * @returns {winston.Logger} Configured Winston logger instance with error handling
12 | */
13 | function createLogger() {
14 | // Helper function to check if an error should be filtered
15 | const shouldFilterError = (error) => {
16 | if (!error) return false;
17 | const errorString = JSON.stringify(error);
18 | return errorString.includes("'app' is missing 'framework'");
19 | };
20 |
21 | // Override console.error to catch native error output
22 | const originalConsoleError = console.error;
23 | console.error = (...args) => {
24 | const errorString = args.join(' ');
25 | if (!errorString.includes("'app' is missing 'framework'")) {
26 | originalConsoleError.apply(console, args);
27 | }
28 | };
29 |
30 | // Capture stderr write to filter error messages
31 | const { write: stdErrWrite } = process.stderr;
32 | process.stderr.write = function (chunk, encoding, callback) {
33 | if (typeof chunk === 'string' && !chunk.includes("'app' is missing 'framework'")) {
34 | stdErrWrite.apply(process.stderr, arguments);
35 | }
36 | if (typeof callback === 'function') callback();
37 | };
38 |
39 | // Define custom format combining timestamp, colors and structured output
40 | const chalk = require('chalk');
41 |
42 | const customFormat = format.combine(
43 | format.timestamp({
44 | format: 'HH:mm:ss'
45 | }),
46 | format((info) => {
47 | if (shouldFilterError(info) ||
48 | shouldFilterError(info.message) ||
49 | shouldFilterError(info.stack) ||
50 | (info.error && shouldFilterError(info.error))) {
51 | return false;
52 | }
53 | return info;
54 | })(),
55 | format.errors({ stack: true }),
56 | format.splat(),
57 | format.json(),
58 | format.colorize(),
59 | format.printf(({ level, message, timestamp, ...metadata }) => {
60 | let msg = `${chalk.hex('#a8a8a8')(timestamp)}${chalk.dim(' ▏')} ${message}`;
61 | if (Object.keys(metadata).length > 0) {
62 | msg += ` ${JSON.stringify(metadata)}`;
63 | }
64 | return msg;
65 | })
66 | );
67 |
68 | // Initialize logger with production-ready configuration
69 | const logger = winston.createLogger({
70 | level: process.env.LOG_LEVEL || 'info',
71 | format: customFormat,
72 | transports: [
73 | new transports.Console({
74 | handleExceptions: true,
75 | handleRejections: true,
76 | format: customFormat
77 | }),
78 | new transports.File({
79 | filename: 'logs/error.log',
80 | level: 'error',
81 | maxsize: 5242880, // 5MB
82 | maxFiles: 5
83 | }),
84 | new transports.File({
85 | filename: 'logs/combined.log',
86 | maxsize: 5242880, // 5MB
87 | maxFiles: 5
88 | })
89 | ],
90 | exitOnError: false,
91 | handleExceptions: true,
92 | handleRejections: true,
93 | silent: false
94 | });
95 |
96 | // Override the default exception handlers
97 | logger.exceptions.handle = function(error) {
98 | if (shouldFilterError(error)) return;
99 | return true;
100 | };
101 |
102 | logger.rejections.handle = function(error) {
103 | if (shouldFilterError(error)) return;
104 | return true;
105 | };
106 |
107 | // Add custom error handler for the process
108 | const errorHandler = (error) => {
109 | if (!shouldFilterError(error)) {
110 | logger.error('Error:', error);
111 | }
112 | };
113 |
114 | process.on('uncaughtException', errorHandler);
115 | process.on('unhandledRejection', errorHandler);
116 |
117 | // Intercept console.log calls and redirect to Winston
118 | const originalConsole = console.log;
119 | console.log = (...args) => {
120 | const message = args
121 | .map(arg => typeof arg === 'object' ?
122 | JSON.stringify(arg, null, 2) :
123 | String(arg))
124 | .join(' ');
125 | logger.info(message);
126 | };
127 |
128 | return logger;
129 | }
130 |
131 | module.exports = createLogger;
--------------------------------------------------------------------------------
/handlers/getPteroUser.js:
--------------------------------------------------------------------------------
1 | const fetch = require("node-fetch");
2 | const loadConfig = require("../handlers/config");
3 | const settings = loadConfig("./config.toml");
4 |
5 | module.exports = (userid, db) => {
6 | return new Promise(async (resolve, err) => {
7 | let cacheaccount = await fetch(
8 | settings.pterodactyl.domain +
9 | "/api/application/users/" +
10 | (await db.get("users-" + userid)) +
11 | "?include=servers",
12 | {
13 | method: "get",
14 | headers: {
15 | "Content-Type": "application/json",
16 | Authorization: `Bearer ${settings.pterodactyl.key}`,
17 | },
18 | }
19 | );
20 | if ((await cacheaccount.statusText) === "Not Found")
21 | return err("Pterodactyl account not found!");
22 | let cacheaccountinfo = JSON.parse(await cacheaccount.text());
23 | resolve(cacheaccountinfo);
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/handlers/getServers.js:
--------------------------------------------------------------------------------
1 | const fetch = require("node-fetch");
2 | const loadConfig = require("../handlers/config");
3 | const settings = loadConfig("./config.toml");
4 |
5 | module.exports = () => {
6 | return new Promise(async (resolve) => {
7 | const allServers = [];
8 |
9 | async function getServersOnPage(page) {
10 | return (
11 | await fetch(
12 | settings.pterodactyl.domain +
13 | "/api/application/servers/?page=" +
14 | page,
15 | {
16 | headers: {
17 | Authorization: `Bearer ${settings.pterodactyl.key}`,
18 | },
19 | }
20 | )
21 | ).json();
22 | }
23 |
24 | let currentPage = 1;
25 | while (true) {
26 | const page = await getServersOnPage(currentPage);
27 | allServers.push(...page.data);
28 | if (page.meta.pagination.total_pages > currentPage) {
29 | currentPage++;
30 | } else {
31 | break;
32 | }
33 | }
34 |
35 | resolve(allServers);
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/handlers/log.js:
--------------------------------------------------------------------------------
1 | const loadConfig = require("../handlers/config");
2 | const settings = loadConfig("./config.toml");
3 | const fetch = require('node-fetch')
4 |
5 | /**
6 | * Log an action to a Discord webhook.
7 | * @param {string} action
8 | * @param {string} message
9 | */
10 | module.exports = (action, message) => {
11 | if (!settings.logging.status) return
12 | if (!settings.logging.actions.user[action] && !settings.logging.actions.admin[action]) return
13 |
14 | console.log(action, message);
15 |
16 | fetch(settings.logging.webhook, {
17 | method: 'POST',
18 | headers: {
19 | 'content-type': 'application/json'
20 | },
21 | body: JSON.stringify({
22 | embeds: [
23 | {
24 | color: hexToDecimal('#FFFFFF'),
25 | title: `Event: \`${action}\``,
26 | description: message,
27 | author: {
28 | name: 'Heliactyl Logging'
29 | },
30 | thumbnail: {
31 | url: 'https://atqr.pages.dev/favicon2.png' // This is the default Heliactyl logo, you can change it if you want.
32 | }
33 | }
34 | ]
35 | })
36 | })
37 | .catch(() => {})
38 | }
39 |
40 | function hexToDecimal(hex) {
41 | return parseInt(hex.replace("#", ""), 16)
42 | }
--------------------------------------------------------------------------------
/handlers/vpnCheck.js:
--------------------------------------------------------------------------------
1 | const fetch = require("node-fetch");
2 | const ejs = require("ejs");
3 | const { renderFile } = require("ejs");
4 | const loadConfig = require("../handlers/config");
5 |
6 | module.exports = (key, db, ip, res) => {
7 | return new Promise(async (resolve) => {
8 | let ipcache = await db.get(`vpncheckcache-${ip}`);
9 | if (!ipcache) {
10 | vpncheck = await (
11 | await fetch(`https://proxycheck.io/v2/${ip}?key=${key}&vpn=1`)
12 | )
13 | .json()
14 | .catch(() => {});
15 | }
16 | if (ipcache || (vpncheck && vpncheck[ip])) {
17 | if (!ipcache) ipcache = vpncheck[ip].proxy;
18 | await db.set(`vpncheckcache-${ip}`, ipcache, 172800000);
19 | // Is a VPN/proxy?
20 | if (ipcache === "yes") {
21 | resolve(true);
22 | return res.send('VPN Detected! Please disable your VPN to continue.');
23 | } else return resolve(false);
24 | } else return resolve(false);
25 | });
26 | };
27 |
--------------------------------------------------------------------------------
/modules/api.js:
--------------------------------------------------------------------------------
1 | const loadConfig = require("../handlers/config");
2 | const settings = loadConfig("./config.toml");
3 |
4 | /* Ensure platform release target is met */
5 | const PrismModule = {
6 | "name": "API v5",
7 | "api_level": 3,
8 | "target_platform": "0.5.0"
9 | };
10 |
11 | if (PrismModule.target_platform !== settings.version) {
12 | console.log('Module ' + PrismModule.name + ' does not support this platform release of Prism. The module was built for platform ' + PrismModule.target_platform + ' but is attempting to run on version ' + settings.version + '.')
13 | process.exit()
14 | }
15 |
16 | /* Module */
17 | module.exports.PrismModule = PrismModule;
18 | module.exports.load = async function(app, db) {
19 | app.get("/api/state", async (req, res) => {
20 | const userId = req.session.userinfo.id;
21 | if (!userId) {
22 | return res.status(401).json({
23 | error: "Not authenticated"
24 | });
25 | } else {
26 | return res.json({
27 | message: "Authenticated",
28 | user: req.session.userinfo
29 | });
30 | }
31 | });
32 |
33 | app.get("/api/coins", async (req, res) => {
34 | if (!req.session.userinfo) {
35 | return res.status(401).json({
36 | error: "Not authenticated"
37 | });
38 | }
39 | const userId = req.session.userinfo.id;
40 | const coins = await db.get(`coins-${userId}`) || 0;
41 | res.json({
42 | coins
43 | });
44 | });
45 |
46 | // User
47 | app.get("/api/user", async (req, res) => {
48 | if (!req.session.userinfo) {
49 | return res.status(401).json({
50 | error: "Not authenticated"
51 | });
52 | }
53 | res.json(req.session.userinfo);
54 | });
55 | }
--------------------------------------------------------------------------------
/modules/extras.js:
--------------------------------------------------------------------------------
1 | /**
2 | * __ ___ __ __
3 | * / /_ ___ / (_)___ ______/ /___ __/ /
4 | * / __ \/ _ \/ / / __ `/ ___/ __/ / / / /
5 | * / / / / __/ / / /_/ / /__/ /_/ /_/ / /
6 | * /_/ /_/\___/_/_/\__,_/\___/\__/\__, /_/
7 | * /____/
8 | *
9 | * Heliactyl 19.0.0 (Bristol Ridge)
10 | *
11 | */
12 |
13 | const loadConfig = require("../handlers/config.js");
14 | const settings = loadConfig("./config.toml");
15 | const fs = require("fs");
16 | const indexjs = require("../app.js");
17 | const fetch = require("node-fetch");
18 | const Queue = require("../handlers/Queue.js");
19 |
20 | /* Ensure platform release target is met */
21 | const PrismModule = { "name": "Extra Features", "api_level": 3, "target_platform": "0.5.0" };
22 |
23 | if (PrismModule.target_platform !== settings.version) {
24 | console.log('Module ' + PrismModule.name + ' does not support this platform release of Prism. The module was built for platform ' + PrismModule.target_platform + ' but is attempting to run on version ' + settings.version + '.')
25 | process.exit()
26 | }
27 |
28 | /* Module */
29 | module.exports.PrismModule = PrismModule;
30 | module.exports.load = async function(app, db) {
31 | app.get(`/api/password`, async (req, res) => {
32 | if (!req.session.userinfo.id) return res.redirect("/login");
33 |
34 | let checkPassword = await db.get("password-" + req.session.userinfo.id);
35 |
36 | if (checkPassword) {
37 | return res.json({ password: checkPassword });
38 | } else {
39 | let newpassword = makeid(settings.api.client.passwordgenerator["length"]);
40 |
41 | await fetch(
42 | settings.pterodactyl.domain + "/api/application/users/" + req.session.pterodactyl.id,
43 | {
44 | method: "patch",
45 | headers: {
46 | 'Content-Type': 'application/json',
47 | "Authorization": `Bearer ${settings.pterodactyl.key}`
48 | },
49 | body: JSON.stringify({
50 | username: req.session.pterodactyl.username,
51 | email: req.session.pterodactyl.email,
52 | first_name: req.session.pterodactyl.first_name,
53 | last_name: req.session.pterodactyl.last_name,
54 | password: newpassword
55 | })
56 | }
57 | );
58 |
59 | await db.set("password-" + req.session.userinfo.id, newpassword)
60 | return res.json({ password: newpassword });
61 | }
62 | });
63 |
64 | app.get("/panel", async (req, res) => {
65 | res.redirect(settings.pterodactyl.domain);
66 | });
67 |
68 | app.get("/notifications", async (req, res) => {
69 | if (!req.session.pterodactyl) return res.redirect("/login");
70 |
71 | let notifications = await db.get('notifications-' + req.session.userinfo.id) || [];
72 |
73 | res.json(notifications)
74 | });
75 |
76 | app.get("/regen", async (req, res) => {
77 | if (!req.session.pterodactyl) return res.redirect("/login");
78 | if (settings.api.client.allow.regen !== true) return res.send("You cannot regenerate your password currently.");
79 |
80 | let newpassword = makeid(settings.api.client.passwordgenerator["length"]);
81 | req.session.password = newpassword;
82 |
83 | await updatePassword(req.session.pterodactyl, newpassword, settings, db);
84 | res.redirect("/security");
85 | });
86 |
87 | app.post("/api/password/change", async (req, res) => {
88 | if (!req.session.pterodactyl) return res.status(401).json({ error: "Unauthorized" });
89 | if (!settings.api.client.allow.regen) return res.status(403).json({ error: "Password changes are not allowed" });
90 |
91 | const { password, confirmPassword } = req.body;
92 |
93 | // Validate password
94 | if (!password || typeof password !== 'string') {
95 | return res.status(400).json({ error: "Invalid password provided" });
96 | }
97 |
98 | if (password !== confirmPassword) {
99 | return res.status(400).json({ error: "Passwords do not match" });
100 | }
101 |
102 | // Password requirements
103 | const minLength = 8;
104 | const hasNumber = /\d/.test(password);
105 | const hasUpperCase = /[A-Z]/.test(password);
106 | const hasLowerCase = /[a-z]/.test(password);
107 | const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
108 |
109 | if (password.length < minLength) {
110 | return res.status(400).json({ error: `Password must be at least ${minLength} characters long` });
111 | }
112 |
113 | if (!(hasNumber && hasUpperCase && hasLowerCase)) {
114 | return res.status(400).json({
115 | error: "Password must contain at least one number, one uppercase letter, and one lowercase letter"
116 | });
117 | }
118 |
119 | try {
120 | await updatePassword(req.session.pterodactyl, password, settings, db);
121 | res.json({ success: true, message: "Password updated successfully" });
122 | } catch (error) {
123 | console.error("Password update error:", error);
124 | res.status(500).json({ error: "Failed to update password" });
125 | }
126 | });
127 |
128 | // Helper function to update password
129 | async function updatePassword(userInfo, newPassword, settings, db) {
130 | await fetch(
131 | `${settings.pterodactyl.domain}/api/application/users/${userInfo.id}`,
132 | {
133 | method: "patch",
134 | headers: {
135 | 'Content-Type': 'application/json',
136 | "Authorization": `Bearer ${settings.pterodactyl.key}`
137 | },
138 | body: JSON.stringify({
139 | username: userInfo.username,
140 | email: userInfo.email,
141 | first_name: userInfo.first_name,
142 | last_name: userInfo.last_name,
143 | password: newPassword
144 | })
145 | }
146 | );
147 |
148 | await db.set("password-" + userInfo.id, newPassword);
149 | }
150 | };
151 |
152 | function makeid(length) {
153 | let result = '';
154 | let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
155 | let charactersLength = characters.length;
156 | for (let i = 0; i < length; i++) {
157 | result += characters.charAt(Math.floor(Math.random() * charactersLength));
158 | }
159 | return result;
160 | }
--------------------------------------------------------------------------------
/modules/referrals.js:
--------------------------------------------------------------------------------
1 | const indexjs = require("../app.js");
2 | const adminjs = require("./admin.js");
3 | const fs = require("fs");
4 | const ejs = require("ejs");
5 | const fetch = require('node-fetch');
6 |
7 | /* Ensure platform release target is met */
8 | const PrismModule = { "name": "Referrals (legacy)", "api_level": 1, "target_platform": "0.5.0" };
9 |
10 | /* Module */
11 | module.exports.PrismModule = PrismModule;
12 | module.exports.load = async function (app, db) {
13 | app.get('/generate', async (req, res) => {
14 | if (!req.session) return res.redirect("/login");
15 | if (!req.session.pterodactyl) return res.redirect("/login");
16 |
17 | if (!req.query.code) {
18 | return res.json({ error: "No code provided" });
19 | }
20 |
21 | let referralCode = req.query.code;
22 | // check if the referral code is less than 16 characters and has no spaces
23 | if(referralCode.length > 15 || referralCode.includes(" ")) {
24 | return res.json({ error: "Invalid code" });
25 | }
26 | // check if the referral code already exists
27 | if(await db.get(referralCode)){
28 | return res.json({ error: "Code already exists" });
29 | }
30 | // Save the referral code in the Keyv store along with the user's information
31 | await db.set(referralCode, {
32 | userId: req.session.userinfo.id,
33 | createdAt: new Date()
34 | });
35 |
36 | // Render the referral code view
37 | res.json({ success: "Referral code created" });
38 | });
39 |
40 | app.get('/claim', async (req, res) => {
41 | if (!req.session) return res.redirect("/login");
42 | if (!req.session.pterodactyl) return res.redirect("/login");
43 |
44 | // Get the referral code from the request body
45 | if (!req.query.code) {
46 | return res.json({ error: "No code provided" });
47 | }
48 |
49 | const referralCode = req.query.code;
50 |
51 | // Retrieve the referral code from the Keyv store
52 | const referral = await db.get(referralCode);
53 |
54 | if (!referral) {
55 | return res.json({ error: "Invalid code" });
56 | }
57 |
58 | // Check if user has already claimed a code
59 | if (await db.get("referral-" + req.session.userinfo.id) == "1") {
60 | return res.json({ error: "Already claimed a code" });
61 | }
62 |
63 | // Check if the referral code was created by the user
64 | if (referral.userId === req.session.userinfo.id) {
65 | // Return an error if the referral code was created by the user
66 | return res.json({ error: "Cannot claim your own code" });
67 | }
68 |
69 | // Award the referral bonus to the user who claimed the code
70 | const ownercoins = await db.get("coins-" + referral.userId);
71 | const usercoins = await db.get("coins-" + req.session.userinfo.id);
72 |
73 | db.set("coins-" + referral.userId, ownercoins + 80)
74 | db.set("coins-" + req.session.userinfo.id, usercoins + 250)
75 | db.set("referral-" + req.session.userinfo.id, 1)
76 |
77 | // Render the referral claimed view
78 | res.json({ success: "Referral code claimed" });
79 | });
80 |
81 | };
--------------------------------------------------------------------------------
/modules/routing.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const fs = require('fs').promises;
4 | const mime = require('mime-types');
5 |
6 | const PrismModule = {
7 | name: "React Panel",
8 | api_level: 3,
9 | target_platform: "0.5.0"
10 | };
11 |
12 | const PROCESSABLE_EXTENSIONS = ['.html', '.js', '.css', '.json'];
13 |
14 | async function processFileContent(content, userinfo) {
15 | if (!userinfo) return content;
16 |
17 | return content.replace(/%user\.(\w+)%/g, (match, key) => {
18 | return userinfo[key] || match;
19 | });
20 | }
21 |
22 | module.exports.PrismModule = PrismModule;
23 | module.exports.load = async function(app, db) {
24 | const distPath = path.join(__dirname, '../app/dist');
25 |
26 | // Redirect / to /app
27 | app.get('/', (req, res) => {
28 | res.redirect('/app');
29 | });
30 |
31 | // Custom middleware to handle all panel files
32 | app.use('/app', async (req, res, next) => {
33 | try {
34 | // Get the relative file path from the URL
35 | let relativePath = req.path;
36 | let filePath = path.join(distPath, relativePath);
37 |
38 | // Check if the file exists
39 | try {
40 | await fs.access(filePath);
41 | const stats = await fs.stat(filePath);
42 |
43 | if (stats.isFile()) {
44 | // If it's a real file, process it
45 | const ext = path.extname(filePath);
46 | const shouldProcess = PROCESSABLE_EXTENSIONS.includes(ext.toLowerCase());
47 |
48 | if (!shouldProcess) {
49 | return res.sendFile(filePath);
50 | }
51 |
52 | const content = await fs.readFile(filePath, 'utf8');
53 | const processedContent = await processFileContent(content, req.session?.userinfo);
54 | const contentType = mime.lookup(filePath) || 'application/octet-stream';
55 | res.type(contentType);
56 | return res.send(processedContent);
57 | }
58 | } catch (err) {
59 | // File doesn't exist or other error - fall through to serve index.html
60 | }
61 |
62 | // If we get here, serve index.html for client-side routing
63 | const indexPath = path.join(distPath, 'index.html');
64 | const content = await fs.readFile(indexPath, 'utf8');
65 | const processedContent = await processFileContent(content, req.session?.userinfo);
66 | res.type('html');
67 | res.send(processedContent);
68 |
69 | } catch (err) {
70 | console.error('Error processing panel file:', err);
71 | res.status(500).send('Internal Server Error');
72 | }
73 | });
74 | };
--------------------------------------------------------------------------------
/modules/server:allocations.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:allocations */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:allocations",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // Get server allocations
23 | router.get('/server/:id/allocations', isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.id;
26 |
27 | // Fetch allocations from Pterodactyl Panel
28 | const response = await axios.get(
29 | `${PANEL_URL}/api/client/servers/${serverId}/network/allocations`,
30 | {
31 | headers: {
32 | 'Authorization': `Bearer ${API_KEY}`,
33 | 'Accept': 'application/json',
34 | },
35 | }
36 | );
37 |
38 | // Transform Pterodactyl's response to the expected format
39 | const allocations = response.data.data.map(allocation => ({
40 | id: allocation.attributes.id,
41 | ip: allocation.attributes.ip,
42 | port: allocation.attributes.port,
43 | is_primary: allocation.attributes.primary,
44 | alias: allocation.attributes.alias || null,
45 | }));
46 |
47 | res.json(allocations);
48 | } catch (error) {
49 | console.error('Error fetching allocations:', error);
50 | res.status(500).json({
51 | error: 'Failed to fetch allocations',
52 | details: error.response?.data || error.message
53 | });
54 | }
55 | });
56 |
57 | // Add new allocation
58 | router.post('/server/:id/allocations', isAuthenticated, ownsServer, async (req, res) => {
59 | try {
60 | const serverId = req.params.id;
61 |
62 | const response = await axios.post(
63 | `${PANEL_URL}/api/client/servers/${serverId}/network/allocations`,
64 | {},
65 | {
66 | headers: {
67 | 'Authorization': `Bearer ${API_KEY}`,
68 | 'Accept': 'application/json',
69 | 'Content-Type': 'application/json',
70 | },
71 | }
72 | );
73 |
74 | // Transform the new allocation to match the expected format
75 | const newAllocation = {
76 | id: response.data.attributes.id,
77 | ip: response.data.attributes.ip,
78 | port: response.data.attributes.port,
79 | is_primary: response.data.attributes.primary,
80 | alias: response.data.attributes.alias || null,
81 | };
82 |
83 | res.status(201).json(newAllocation);
84 | } catch (error) {
85 | console.error('Error adding allocation:', error);
86 | res.status(500).json({
87 | error: 'Failed to add allocation',
88 | details: error.response?.data || error.message
89 | });
90 | }
91 | });
92 |
93 | // Remove allocation
94 | router.delete('/server/:id/allocations/:allocationId', isAuthenticated, ownsServer, async (req, res) => {
95 | try {
96 | const { id: serverId, allocationId } = req.params;
97 |
98 | await axios.delete(
99 | `${PANEL_URL}/api/client/servers/${serverId}/network/allocations/${allocationId}`,
100 | {
101 | headers: {
102 | 'Authorization': `Bearer ${API_KEY}`,
103 | 'Accept': 'application/json',
104 | },
105 | }
106 | );
107 |
108 | res.status(200).json({ message: 'Allocation removed successfully' });
109 | } catch (error) {
110 | console.error('Error removing allocation:', error);
111 | res.status(500).json({
112 | error: 'Failed to remove allocation',
113 | details: error.response?.data || error.message
114 | });
115 | }
116 | });
117 |
118 | app.use("/api", router);
119 | };
--------------------------------------------------------------------------------
/modules/server:backups.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:backups */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:backups",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // GET /api/server/:id/backups - List backups
23 | router.get("/server/:id/backups", isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.id;
26 | const response = await axios.get(
27 | `${PANEL_URL}/api/client/servers/${serverId}/backups`,
28 | {
29 | headers: {
30 | Authorization: `Bearer ${API_KEY}`,
31 | Accept: "application/json",
32 | "Content-Type": "application/json",
33 | },
34 | }
35 | );
36 | res.json(response.data);
37 | } catch (error) {
38 | console.error("Error fetching backups:", error);
39 | res.status(500).json({ error: "Internal server error" });
40 | }
41 | });
42 |
43 | // POST /api/server/:id/backups - Create backup
44 | router.post("/server/:id/backups", isAuthenticated, ownsServer, async (req, res) => {
45 | try {
46 | const serverId = req.params.id;
47 | const response = await axios.post(
48 | `${PANEL_URL}/api/client/servers/${serverId}/backups`,
49 | {},
50 | {
51 | headers: {
52 | Authorization: `Bearer ${API_KEY}`,
53 | Accept: "application/json",
54 | "Content-Type": "application/json",
55 | },
56 | }
57 | );
58 | res.status(201).json(response.data);
59 | } catch (error) {
60 | console.error("Error creating backup:", error);
61 | res.status(500).json({ error: "Internal server error" });
62 | }
63 | });
64 |
65 | // GET /api/server/:id/backups/:backupId/download - Get backup download URL
66 | router.get(
67 | "/server/:id/backups/:backupId/download",
68 | isAuthenticated,
69 | ownsServer,
70 | async (req, res) => {
71 | try {
72 | const serverId = req.params.id;
73 | const backupId = req.params.backupId;
74 | const response = await axios.get(
75 | `${PANEL_URL}/api/client/servers/${serverId}/backups/${backupId}/download`,
76 | {
77 | headers: {
78 | Authorization: `Bearer ${API_KEY}`,
79 | Accept: "application/json",
80 | "Content-Type": "application/json",
81 | },
82 | }
83 | );
84 | res.json(response.data);
85 | } catch (error) {
86 | console.error("Error generating backup download link:", error);
87 | res.status(500).json({ error: "Internal server error" });
88 | }
89 | }
90 | );
91 |
92 | // DELETE /api/server/:id/backups/:backupId - Delete backup
93 | router.delete(
94 | "/server/:id/backups/:backupId",
95 | isAuthenticated,
96 | ownsServer,
97 | async (req, res) => {
98 | try {
99 | const serverId = req.params.id;
100 | const backupId = req.params.backupId;
101 | await axios.delete(
102 | `${PANEL_URL}/api/client/servers/${serverId}/backups/${backupId}`,
103 | {
104 | headers: {
105 | Authorization: `Bearer ${API_KEY}`,
106 | Accept: "application/json",
107 | "Content-Type": "application/json",
108 | },
109 | }
110 | );
111 | res.status(204).send();
112 | } catch (error) {
113 | console.error("Error deleting backup:", error);
114 | res.status(500).json({ error: "Internal server error" });
115 | }
116 | }
117 | );
118 |
119 | app.use("/api", router);
120 | };
--------------------------------------------------------------------------------
/modules/server:files_delete.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:files_delete */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, logActivity, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:files_delete",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // POST /api/server/:id/files/delete
23 | router.post("/server/:id/files/delete", isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.id;
26 | const { root, files } = req.body;
27 |
28 | if (!files || !Array.isArray(files) || files.length === 0) {
29 | return res.status(400).json({ error: 'Files array is required' });
30 | }
31 |
32 | await axios.post(
33 | `${PANEL_URL}/api/client/servers/${serverId}/files/delete`,
34 | { root, files },
35 | {
36 | headers: {
37 | Authorization: `Bearer ${API_KEY}`,
38 | Accept: "application/json",
39 | "Content-Type": "application/json",
40 | },
41 | }
42 | );
43 |
44 | await logActivity(db, serverId, 'Delete Files', { root, files });
45 | res.status(204).send();
46 | } catch (error) {
47 | console.error("Error deleting files:", error);
48 | res.status(500).json({ error: "Internal server error" });
49 | }
50 | });
51 |
52 | // POST /api/server/:id/files/compress
53 | router.post("/server/:id/files/compress", isAuthenticated, ownsServer, async (req, res) => {
54 | try {
55 | const serverId = req.params.id;
56 | const { root, files } = req.body;
57 |
58 | if (!files || !Array.isArray(files) || files.length === 0) {
59 | return res.status(400).json({ error: 'Files array is required' });
60 | }
61 |
62 | const response = await axios.post(
63 | `${PANEL_URL}/api/client/servers/${serverId}/files/compress`,
64 | { root, files },
65 | {
66 | headers: {
67 | Authorization: `Bearer ${API_KEY}`,
68 | Accept: "application/json",
69 | "Content-Type": "application/json",
70 | },
71 | }
72 | );
73 |
74 | await logActivity(db, serverId, 'Compress Files', { root, files });
75 | res.status(200).json(response.data);
76 | } catch (error) {
77 | console.error("Error compressing files:", error);
78 | res.status(500).json({ error: "Internal server error" });
79 | }
80 | });
81 |
82 | // POST /api/server/:id/files/decompress
83 | router.post("/server/:id/files/decompress", isAuthenticated, ownsServer, async (req, res) => {
84 | try {
85 | const serverId = req.params.id;
86 | const { root, file } = req.body;
87 |
88 | if (!file) {
89 | return res.status(400).json({ error: 'File is required' });
90 | }
91 |
92 | await axios.post(
93 | `${PANEL_URL}/api/client/servers/${serverId}/files/decompress`,
94 | { root, file },
95 | {
96 | headers: {
97 | Authorization: `Bearer ${API_KEY}`,
98 | Accept: "application/json",
99 | "Content-Type": "application/json",
100 | },
101 | }
102 | );
103 |
104 | await logActivity(db, serverId, 'Decompress File', { root, file });
105 | res.status(204).send();
106 | } catch (error) {
107 | console.error("Error decompressing file:", error);
108 | res.status(500).json({ error: "Internal server error" });
109 | }
110 | });
111 |
112 | app.use("/api", router);
113 | };
--------------------------------------------------------------------------------
/modules/server:files_list.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:files_list */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:files_list",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // GET /api/server/:id/files/list
23 | router.get("/server/:id/files/list", isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.id;
26 | const directory = req.query.directory || "/";
27 | const page = parseInt(req.query.page) || 1;
28 | const perPage = parseInt(req.query.per_page) || 10;
29 |
30 | const response = await axios.get(
31 | `${PANEL_URL}/api/client/servers/${serverId}/files/list`,
32 | {
33 | params: {
34 | directory,
35 | page: page,
36 | per_page: perPage
37 | },
38 | headers: {
39 | Authorization: `Bearer ${API_KEY}`,
40 | Accept: "application/json",
41 | "Content-Type": "application/json",
42 | },
43 | }
44 | );
45 |
46 | // Add pagination metadata to the response
47 | const totalItems = response.data.meta?.pagination?.total || 0;
48 | const totalPages = Math.ceil(totalItems / perPage);
49 |
50 | const paginatedResponse = {
51 | ...response.data,
52 | meta: {
53 | ...response.data.meta,
54 | pagination: {
55 | ...response.data.meta?.pagination,
56 | current_page: page,
57 | per_page: perPage,
58 | total_pages: totalPages
59 | }
60 | }
61 | };
62 |
63 | res.json(paginatedResponse);
64 | } catch (error) {
65 | console.error("Error listing files:", error);
66 | res.status(500).json({ error: "Internal server error" });
67 | }
68 | });
69 |
70 | app.use("/api", router);
71 | };
--------------------------------------------------------------------------------
/modules/server:files_read.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:files_read */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:files_read",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // GET /api/server/:id/files/contents
23 | router.get("/server/:id/files/contents", isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.id;
26 | const file = encodeURIComponent(req.query.file); // URL-encode the file path
27 |
28 | const response = await axios.get(
29 | `${PANEL_URL}/api/client/servers/${serverId}/files/contents?file=${file}`,
30 | {
31 | headers: {
32 | Authorization: `Bearer ${API_KEY}`,
33 | Accept: "application/json",
34 | "Content-Type": "application/json",
35 | },
36 | responseType: "text", // Treat the response as plain text
37 | }
38 | );
39 |
40 | // Send the raw file content back to the client
41 | res.send(response.data);
42 | } catch (error) {
43 | console.error("Error getting file contents:", error);
44 | res.status(500).json({ error: "Internal server error" });
45 | }
46 | });
47 |
48 | // GET /api/server/:id/files/download
49 | router.get("/server/:id/files/download", isAuthenticated, ownsServer, async (req, res) => {
50 | try {
51 | const serverId = req.params.id;
52 | const file = req.query.file;
53 |
54 | const response = await axios.get(
55 | `${PANEL_URL}/api/client/servers/${serverId}/files/download`,
56 | {
57 | params: { file },
58 | headers: {
59 | Authorization: `Bearer ${API_KEY}`,
60 | Accept: "application/json",
61 | "Content-Type": "application/json",
62 | },
63 | }
64 | );
65 |
66 | res.json(response.data);
67 | } catch (error) {
68 | console.error("Error getting download link:", error);
69 | res.status(500).json({ error: "Internal server error" });
70 | }
71 | });
72 |
73 | app.use("/api", router);
74 | };
--------------------------------------------------------------------------------
/modules/server:files_transfer.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:files_transfer */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, logActivity, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:files_transfer",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // GET /api/server/:id/files/upload
23 | router.get("/server/:id/files/upload", isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.id;
26 | const directory = req.query.directory || "/";
27 |
28 | const response = await axios.get(
29 | `${PANEL_URL}/api/client/servers/${serverId}/files/upload`,
30 | {
31 | params: { directory },
32 | headers: {
33 | Authorization: `Bearer ${API_KEY}`,
34 | Accept: "application/json",
35 | "Content-Type": "application/json",
36 | },
37 | }
38 | );
39 |
40 | res.json(response.data);
41 | } catch (error) {
42 | console.error("Error getting upload URL:", error);
43 | res.status(500).json({ error: "Internal server error" });
44 | }
45 | });
46 |
47 | // POST /api/server/:id/files/copy
48 | router.post("/server/:id/files/copy", isAuthenticated, ownsServer, async (req, res) => {
49 | try {
50 | const serverId = req.params.id;
51 | const { location } = req.body;
52 |
53 | if (!location) {
54 | return res.status(400).json({ error: 'Location is required' });
55 | }
56 |
57 | await axios.post(
58 | `${PANEL_URL}/api/client/servers/${serverId}/files/copy`,
59 | { location },
60 | {
61 | headers: {
62 | Authorization: `Bearer ${API_KEY}`,
63 | Accept: "application/json",
64 | "Content-Type": "application/json",
65 | },
66 | }
67 | );
68 |
69 | await logActivity(db, serverId, 'Copy Files', { location });
70 | res.status(204).send();
71 | } catch (error) {
72 | console.error("Error copying files:", error);
73 | res.status(500).json({ error: "Internal server error" });
74 | }
75 | });
76 |
77 | async function getAvailableAllocations(nodeId) {
78 | const response = await axios.get(
79 | `${PANEL_URL}/nodes/${nodeId}/allocations?per_page=10000`,
80 | {
81 | headers: {
82 | Authorization: `Bearer ${API_KEY}`,
83 | Accept: "application/json",
84 | "Content-Type": "application/json",
85 | },
86 | }
87 | );
88 | return response.data.data.filter(allocation => !allocation.attributes.assigned);
89 | }
90 |
91 | // GET server details helper
92 | async function getServerDetails(serverId) {
93 | const response = await axios.get(
94 | `${PANEL_URL}/api/application/servers/${serverId}`,
95 | {
96 | headers: {
97 | Authorization: `Bearer ${API_KEY}`,
98 | Accept: "application/json",
99 | "Content-Type": "application/json",
100 | },
101 | }
102 | );
103 | return response.data.data;
104 | }
105 |
106 | // Server transfer endpoint
107 | router.get("/server/transfer", isAuthenticated, async (req, res) => {
108 | const { id, nodeId } = req.query;
109 | const userId = req.session.pterodactyl.id;
110 |
111 | if (!id || !nodeId) {
112 | return res.status(400).json({ error: "Missing required parameters: id or nodeId" });
113 | }
114 |
115 | try {
116 | const server = await getServerDetails(id);
117 | const availableAllocations = await getAvailableAllocations(nodeId);
118 |
119 | if (availableAllocations.length === 0) {
120 | return res.status(500).json({ error: "No available allocations on the target node" });
121 | }
122 |
123 | await axios.post(
124 | `${PANEL_URL}/admin/servers/view/${id}/manage/transfer`,
125 | {
126 | node_id: nodeId,
127 | allocation_id: availableAllocations[0].attributes.id
128 | },
129 | {
130 | headers: {
131 | Authorization: `Bearer ${API_KEY}`,
132 | Accept: "application/json",
133 | "Content-Type": "application/json",
134 | },
135 | }
136 | );
137 |
138 | await logActivity(db, id, 'Server Transfer', { nodeId });
139 | res.status(200).json({
140 | message: `Transfer for server ${id} to node ${nodeId} initiated.`,
141 | });
142 | } catch (error) {
143 | console.error("Error transferring server:", error);
144 | res.status(500).json({ error: "Internal server error" });
145 | }
146 | });
147 |
148 | app.use("/api", router);
149 | };
--------------------------------------------------------------------------------
/modules/server:files_write.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:files_write */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, logActivity, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:files_write",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // POST /api/server/:id/files/write
23 | router.post("/server/:id/files/write", isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | console.log('Saving ' + req.query.file + ' to server ' + req.params.id);
26 | const serverId = req.params.id;
27 | const file = req.query.file;
28 | const content = req.body;
29 |
30 | const response = await axios.post(
31 | `${PANEL_URL}/api/client/servers/${serverId}/files/write?file=${file}`,
32 | content,
33 | {
34 | headers: {
35 | Authorization: `Bearer ${API_KEY}`,
36 | Accept: "application/json",
37 | "Content-Type": "text/plain",
38 | },
39 | }
40 | );
41 |
42 | // Log response status & text if error
43 | if (response.status !== 204) {
44 | console.error("Error writing file:", response.statusText);
45 | return res.status(response.status).json({ error: response.statusText });
46 | } else {
47 | await logActivity(db, serverId, 'Write File', { file });
48 | res.status(204).send();
49 | }
50 |
51 | await logActivity(db, serverId, 'Write File', { file });
52 | res.status(204).send();
53 | } catch (error) {
54 | console.error("Error writing file:", error);
55 | res.status(500).json({ error: "Internal server error" });
56 | }
57 | });
58 |
59 | // POST /api/server/:id/files/create-folder
60 | router.post("/server/:id/files/create-folder", isAuthenticated, ownsServer, async (req, res) => {
61 | try {
62 | const serverId = req.params.id;
63 | const { root, name } = req.body;
64 |
65 | if (!name) {
66 | return res.status(400).json({ error: 'Folder name is required' });
67 | }
68 |
69 | await axios.post(
70 | `${PANEL_URL}/api/client/servers/${serverId}/files/create-folder`,
71 | { root, name },
72 | {
73 | headers: {
74 | Authorization: `Bearer ${API_KEY}`,
75 | Accept: "application/json",
76 | "Content-Type": "application/json",
77 | },
78 | }
79 | );
80 |
81 | await logActivity(db, serverId, 'Create Folder', { root, name });
82 | res.status(204).send();
83 | } catch (error) {
84 | console.error("Error creating folder:", error);
85 | res.status(500).json({ error: "Internal server error" });
86 | }
87 | });
88 |
89 | // PUT /api/server/:id/files/rename
90 | router.put("/server/:id/files/rename", isAuthenticated, ownsServer, async (req, res) => {
91 | try {
92 | const serverId = req.params.id;
93 | const { root, files } = req.body;
94 |
95 | if (!files || !Array.isArray(files) || files.length === 0) {
96 | return res.status(400).json({ error: 'Files array is required' });
97 | }
98 |
99 | await axios.put(
100 | `${PANEL_URL}/api/client/servers/${serverId}/files/rename`,
101 | { root, files },
102 | {
103 | headers: {
104 | Authorization: `Bearer ${API_KEY}`,
105 | Accept: "application/json",
106 | "Content-Type": "application/json",
107 | },
108 | }
109 | );
110 |
111 | await logActivity(db, serverId, 'Rename Files', { root, files });
112 | res.status(204).send();
113 | } catch (error) {
114 | console.error("Error renaming file/folder:", error);
115 | res.status(500).json({ error: "Internal server error" });
116 | }
117 | });
118 |
119 | app.use("/api", router);
120 | };
--------------------------------------------------------------------------------
/modules/server:logs.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:logs */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const { isAuthenticated, ownsServer } = require("./server:core.js");
7 |
8 | /* --------------------------------------------- */
9 | /* Prism Module */
10 | /* --------------------------------------------- */
11 | const PrismModule = {
12 | name: "server:logs",
13 | api_level: 3,
14 | target_platform: "0.5.0",
15 | };
16 |
17 | module.exports.PrismModule = PrismModule;
18 | module.exports.load = async function (app, db) {
19 | const router = express.Router();
20 |
21 | // GET /api/server/:id/logs - Get server activity logs
22 | router.get('/server/:id/logs', isAuthenticated, ownsServer, async (req, res) => {
23 | try {
24 | const serverId = req.params.id;
25 | const page = parseInt(req.query.page) || 1;
26 | const limit = parseInt(req.query.limit) || 20;
27 |
28 | // Get logs from database
29 | const activityLog = await db.get(`activity_log_${serverId}`) || [];
30 |
31 | // Calculate pagination
32 | const startIndex = (page - 1) * limit;
33 | const endIndex = startIndex + limit;
34 | const totalLogs = activityLog.length;
35 | const totalPages = Math.ceil(totalLogs / limit);
36 |
37 | // Get paginated logs
38 | const paginatedLogs = activityLog.slice(startIndex, endIndex);
39 |
40 | // Format response with pagination metadata
41 | const response = {
42 | data: paginatedLogs,
43 | pagination: {
44 | current_page: page,
45 | total_pages: totalPages,
46 | total_items: totalLogs,
47 | items_per_page: limit,
48 | has_more: endIndex < totalLogs
49 | }
50 | };
51 |
52 | res.json(response);
53 | } catch (error) {
54 | console.error('Error fetching activity logs:', error);
55 | res.status(500).json({ error: 'Internal server error' });
56 | }
57 | });
58 |
59 | app.use("/api", router);
60 | };
--------------------------------------------------------------------------------
/modules/server:players.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:players */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const { isAuthenticated, ownsServer, sendCommandAndGetResponse } = require("./server:core.js");
7 |
8 | /* --------------------------------------------- */
9 | /* Prism Module */
10 | /* --------------------------------------------- */
11 | const PrismModule = {
12 | name: "server:players",
13 | api_level: 3,
14 | target_platform: "0.5.0",
15 | };
16 |
17 | module.exports.PrismModule = PrismModule;
18 | module.exports.load = async function (app, db) {
19 | const router = express.Router();
20 |
21 | // Get current players
22 | router.get('/server/:id/players', isAuthenticated, ownsServer, async (req, res) => {
23 | try {
24 | const serverId = req.params.id;
25 |
26 | const consoleLines = await sendCommandAndGetResponse(serverId, 'list');
27 |
28 | // Parse player list from console output
29 | const playerListLine = consoleLines.find(line => line.includes('players online:'));
30 | let players = [];
31 |
32 | if (playerListLine) {
33 | const match = playerListLine.match(/There are \d+ of a max of \d+ players online: (.*)/);
34 | if (match && match[1]) {
35 | players = match[1].split(',').map(p => p.trim()).filter(p => p);
36 | }
37 | }
38 |
39 | res.json({ players });
40 | } catch (error) {
41 | console.error('Error getting player list:', error);
42 | res.status(500).json({ error: 'Internal server error' });
43 | }
44 | });
45 |
46 | app.use("/api", router);
47 | };
--------------------------------------------------------------------------------
/modules/server:players_ban.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:players_ban */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const { isAuthenticated, ownsServer, sendCommandAndGetResponse, logActivity } = require("./server:core.js");
7 |
8 | /* --------------------------------------------- */
9 | /* Prism Module */
10 | /* --------------------------------------------- */
11 | const PrismModule = {
12 | name: "server:players_ban",
13 | api_level: 3,
14 | target_platform: "0.5.0",
15 | };
16 |
17 | module.exports.PrismModule = PrismModule;
18 | module.exports.load = async function (app, db) {
19 | const router = express.Router();
20 |
21 | // Kick player
22 | router.post('/server/:id/players/:player/kick', isAuthenticated, ownsServer, async (req, res) => {
23 | try {
24 | const { id: serverId, player } = req.params;
25 | const { reason = 'You have been kicked from the server' } = req.body;
26 |
27 | await sendCommandAndGetResponse(serverId, `kick ${player} ${reason}`, 2000);
28 | await logActivity(db, serverId, 'Kick Player', { player, reason });
29 |
30 | res.json({ success: true });
31 | } catch (error) {
32 | console.error('Error kicking player:', error);
33 | res.status(500).json({ error: 'Internal server error' });
34 | }
35 | });
36 |
37 | // Ban player
38 | router.post('/server/:id/players/:player/ban', isAuthenticated, ownsServer, async (req, res) => {
39 | try {
40 | const { id: serverId, player } = req.params;
41 | const { reason = 'You have been banned from the server' } = req.body;
42 |
43 | await sendCommandAndGetResponse(serverId, `ban ${player} ${reason}`, 2000);
44 | await logActivity(db, serverId, 'Ban Player', { player, reason });
45 |
46 | res.json({ success: true });
47 | } catch (error) {
48 | console.error('Error banning player:', error);
49 | res.status(500).json({ error: 'Internal server error' });
50 | }
51 | });
52 |
53 | // Unban player
54 | router.post('/server/:id/players/:player/unban', isAuthenticated, ownsServer, async (req, res) => {
55 | try {
56 | const { id: serverId, player } = req.params;
57 |
58 | await sendCommandAndGetResponse(serverId, `pardon ${player}`, 2000);
59 | await logActivity(db, serverId, 'Unban Player', { player });
60 |
61 | res.json({ success: true });
62 | } catch (error) {
63 | console.error('Error unbanning player:', error);
64 | res.status(500).json({ error: 'Internal server error' });
65 | }
66 | });
67 |
68 | // Get banned players list
69 | router.get('/server/:id/players/banned', isAuthenticated, ownsServer, async (req, res) => {
70 | try {
71 | const serverId = req.params.id;
72 | const consoleLines = await sendCommandAndGetResponse(serverId, 'banlist');
73 |
74 | // Parse banned players from console output
75 | const bannedPlayers = [];
76 | let collectingBans = false;
77 |
78 | for (const line of consoleLines) {
79 | if (line.includes('Banned players:')) {
80 | collectingBans = true;
81 | continue;
82 | }
83 |
84 | if (collectingBans && line.trim()) {
85 | const players = line.split(',').map(p => p.trim()).filter(p => p);
86 | bannedPlayers.push(...players);
87 | }
88 | }
89 |
90 | res.json({ bannedPlayers });
91 | } catch (error) {
92 | console.error('Error getting banned players:', error);
93 | res.status(500).json({ error: 'Internal server error' });
94 | }
95 | });
96 |
97 | app.use("/api", router);
98 | };
--------------------------------------------------------------------------------
/modules/server:plugins.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:plugins */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const FormData = require("form-data");
8 | const { isAuthenticated, ownsServer, PANEL_URL, API_KEY } = require("./server:core.js");
9 |
10 | /* --------------------------------------------- */
11 | /* Prism Module */
12 | /* --------------------------------------------- */
13 | const PrismModule = {
14 | name: "server:plugins",
15 | api_level: 3,
16 | target_platform: "0.5.0",
17 | };
18 |
19 | module.exports.PrismModule = PrismModule;
20 | module.exports.load = async function (app, db) {
21 | const router = express.Router();
22 |
23 | const SPIGOT_API_BASE = "https://api.spiget.org/v2";
24 |
25 | // GET /api/plugins/list - List plugins
26 | router.get("/plugins/list", async (req, res) => {
27 | try {
28 | const response = await axios.get(`${SPIGOT_API_BASE}/resources`, {
29 | params: {
30 | size: 100,
31 | sort: "-downloads", // Sort by most downloaded
32 | },
33 | });
34 | res.json(response.data);
35 | } catch (error) {
36 | console.error("Error fetching plugin list:", error);
37 | res.status(500).json({ error: "Internal server error" });
38 | }
39 | });
40 |
41 | // GET /api/plugins/search - Search plugins
42 | router.get("/plugins/search", async (req, res) => {
43 | const { query } = req.query;
44 | if (!query) {
45 | return res.status(400).json({ error: "Search query is required" });
46 | }
47 |
48 | try {
49 | const response = await axios.get(
50 | `${SPIGOT_API_BASE}/search/resources/${query}`,
51 | {
52 | params: {
53 | size: 100,
54 | sort: "-downloads",
55 | },
56 | }
57 | );
58 | res.json(response.data);
59 | } catch (error) {
60 | console.error("Error searching plugins:", error);
61 | res.status(500).json({ error: "Internal server error" });
62 | }
63 | });
64 |
65 | // POST /api/plugins/install/:serverId - Install plugin
66 | router.post("/plugins/install/:serverId", isAuthenticated, ownsServer, async (req, res) => {
67 | const { serverId } = req.params;
68 | const { pluginId } = req.body;
69 |
70 | if (!pluginId) {
71 | return res.status(400).json({ error: "Plugin ID is required" });
72 | }
73 |
74 | try {
75 | // Get plugin details
76 | const pluginDetails = await axios.get(
77 | `${SPIGOT_API_BASE}/resources/${pluginId}`
78 | );
79 | const downloadUrl = `https://api.spiget.org/v2/resources/${pluginId}/download`;
80 |
81 | // Download the plugin
82 | const pluginResponse = await axios.get(downloadUrl, {
83 | responseType: "arraybuffer",
84 | });
85 | const pluginBuffer = Buffer.from(pluginResponse.data, "binary");
86 |
87 | // Get upload URL from Pterodactyl
88 | const uploadUrlResponse = await axios.get(
89 | `${PANEL_URL}/api/client/servers/${serverId}/files/upload`,
90 | {
91 | headers: {
92 | Authorization: `Bearer ${API_KEY}`,
93 | Accept: "application/json",
94 | },
95 | }
96 | );
97 |
98 | const uploadUrl = uploadUrlResponse.data.attributes.url;
99 |
100 | // Upload plugin using multipart/form-data
101 | const form = new FormData();
102 | const tempFileName = `temp_${Date.now()}_${pluginId}.jar`;
103 | form.append("files", pluginBuffer, {
104 | filename: tempFileName,
105 | contentType: "application/java-archive",
106 | });
107 |
108 | const headers = form.getHeaders();
109 | await axios.post(uploadUrl, form, {
110 | headers: {
111 | ...headers,
112 | "Content-Length": form.getLengthSync(),
113 | },
114 | });
115 |
116 | // Move plugin to plugins directory
117 | await axios.put(
118 | `${PANEL_URL}/api/client/servers/${serverId}/files/rename`,
119 | {
120 | root: "/",
121 | files: [
122 | {
123 | from: tempFileName,
124 | to: `plugins/${pluginDetails.data.name}.jar`,
125 | },
126 | ],
127 | },
128 | {
129 | headers: {
130 | Authorization: `Bearer ${API_KEY}`,
131 | Accept: "application/json",
132 | },
133 | }
134 | );
135 |
136 | res.json({ message: "Plugin installed successfully" });
137 | } catch (error) {
138 | console.error("Error installing plugin:", error);
139 | res.status(500).json({ error: "Internal server error" });
140 | }
141 | });
142 |
143 | app.use("/api", router);
144 | };
--------------------------------------------------------------------------------
/modules/server:power.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:power */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const loadConfig = require("../handlers/config");
8 | const settings = loadConfig("./config.toml");
9 | const { isAuthenticated, ownsServer, logActivity, PANEL_URL, API_KEY } = require("./server:core.js");
10 |
11 | /* --------------------------------------------- */
12 | /* Prism Module */
13 | /* --------------------------------------------- */
14 | const PrismModule = {
15 | name: "server:power",
16 | api_level: 3,
17 | target_platform: "0.5.0",
18 | };
19 |
20 | module.exports.PrismModule = PrismModule;
21 | module.exports.load = async function (app, db) {
22 | if (PrismModule.target_platform !== settings.version) {
23 | console.log(
24 | "Module " +
25 | PrismModule.name +
26 | " does not support this platform release of Prism. The module was built for platform " +
27 | PrismModule.target_platform +
28 | " but is attempting to run on version " +
29 | settings.version +
30 | "."
31 | );
32 | process.exit();
33 | }
34 |
35 | const router = express.Router();
36 |
37 | /**
38 | * Set server power state
39 | * POST /api/server/:id/power
40 | */
41 | router.post("/server/:id/power", isAuthenticated, ownsServer, async (req, res) => {
42 | try {
43 | const serverId = req.params.id;
44 | const { signal } = req.body;
45 |
46 | // Validate power signal
47 | const validSignals = ['start', 'stop', 'restart', 'kill'];
48 | if (!validSignals.includes(signal)) {
49 | return res.status(400).json({ error: 'Invalid power signal' });
50 | }
51 |
52 | const response = await axios.post(
53 | `${PANEL_URL}/api/client/servers/${serverId}/power`,
54 | {
55 | signal: signal,
56 | },
57 | {
58 | headers: {
59 | Accept: "application/json",
60 | "Content-Type": "application/json",
61 | Authorization: `Bearer ${API_KEY}`,
62 | },
63 | }
64 | );
65 |
66 | if (response.status === 204) {
67 | await logActivity(db, serverId, 'Power Action', { signal });
68 | res.status(204).send();
69 | } else {
70 | throw new Error('Unexpected response from panel');
71 | }
72 | } catch (error) {
73 | console.error("Error changing power state:", error);
74 | res.status(500).json({ error: "Internal server error" });
75 | }
76 | });
77 |
78 | /**
79 | * Send command to server
80 | * POST /api/server/:id/command
81 | */
82 | router.post("/server/:id/command", isAuthenticated, ownsServer, async (req, res) => {
83 | try {
84 | const serverId = req.params.id;
85 | const { command } = req.body;
86 |
87 | if (!command) {
88 | return res.status(400).json({ error: 'Command is required' });
89 | }
90 |
91 | await sendCommandAndGetResponse(serverId, command);
92 | await logActivity(db, serverId, 'Send Command', { command });
93 |
94 | res.json({ success: true, message: "Command sent successfully" });
95 | } catch (error) {
96 | console.error("Error sending command:", error);
97 | res.status(500).json({ error: "Internal server error" });
98 | }
99 | });
100 |
101 | // Use the router with the '/api' prefix
102 | app.use("/api", router);
103 | };
--------------------------------------------------------------------------------
/modules/server:settings.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const loadConfig = require("../handlers/config");
3 | const settings = loadConfig("./config.toml");
4 | const WebSocket = require('ws');
5 | const axios = require('axios');
6 |
7 | /* Ensure platform release target is met */
8 | const PrismModule = {
9 | "name": "Settings",
10 | "api_level": 3,
11 | "target_platform": "0.5.0"
12 | };
13 |
14 | if (PrismModule.target_platform !== settings.version) {
15 | console.log('Module ' + PrismModule.name + ' does not support this platform release of Prism. The module was built for platform ' + PrismModule.target_platform + ' but is attempting to run on version ' + settings.version + '.')
16 | process.exit()
17 | }
18 |
19 | /* Module */
20 | module.exports.PrismModule = PrismModule;
21 | module.exports.load = async function(app, db) {
22 | const router = express.Router();
23 |
24 | // Middleware to check if user is authenticated
25 | const isAuthenticated = (req, res, next) => {
26 | if (req.session.pterodactyl) {
27 | next();
28 | } else {
29 | res.status(401).json({ error: "Unauthorized" });
30 | }
31 | };
32 |
33 | // Middleware to check if user owns the server
34 | const ownsServer = (req, res, next) => {
35 | const serverId = req.params.id;
36 | const userServers = req.session.pterodactyl.relationships.servers.data;
37 | const serverOwned = userServers.some(server => server.attributes.identifier === serverId);
38 |
39 | if (serverOwned) {
40 | next();
41 | } else {
42 | res.status(403).json({ error: "Forbidden. You don't have access to this server." });
43 | }
44 | };
45 |
46 | // POST Reinstall server
47 | router.post('/server/:id/reinstall', isAuthenticated, ownsServer, async (req, res) => {
48 | try {
49 | const serverId = req.params.id;
50 | await axios.post(`${settings.pterodactyl.domain}/api/client/servers/${serverId}/settings/reinstall`, {}, {
51 | headers: {
52 | 'Accept': 'application/json',
53 | 'Content-Type': 'application/json',
54 | 'Authorization': `Bearer ${settings.pterodactyl.client_key}`
55 | }
56 | });
57 | res.status(204).send(); // No content response on success
58 | } catch (error) {
59 | console.error('Error reinstalling server:', error);
60 | res.status(500).json({ error: "Internal server error" });
61 | }
62 | });
63 |
64 | // POST Rename server
65 | router.post('/server/:id/rename', isAuthenticated, ownsServer, async (req, res) => {
66 | try {
67 | const serverId = req.params.id;
68 | const { name } = req.body; // Expecting the new name for the server in the request body
69 |
70 | await axios.post(`${settings.pterodactyl.domain}/api/client/servers/${serverId}/settings/rename`,
71 | { name: name },
72 | {
73 | headers: {
74 | 'Accept': 'application/json',
75 | 'Content-Type': 'application/json',
76 | 'Authorization': `Bearer ${settings.pterodactyl.client_key}`
77 | }
78 | });
79 | res.status(204).send(); // No content response on success
80 | } catch (error) {
81 | console.error('Error renaming server:', error);
82 | res.status(500).json({ error: "Internal server error" });
83 | }
84 | });
85 |
86 | // Use the router with the '/api' prefix
87 | app.use('/api', router);
88 | };
--------------------------------------------------------------------------------
/modules/server:startup.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:startup */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:startup",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // PUT /api/server/:id/startup - Update startup configuration
23 | router.put('/server/:serverId/startup', isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.serverId;
26 | const { startup, environment, egg, image, skip_scripts } = req.body;
27 |
28 | // First, get the current server details
29 | const serverDetailsResponse = await axios.get(
30 | `${PANEL_URL}/api/application/servers/${serverId}?include=container`,
31 | {
32 | headers: {
33 | 'Authorization': `Bearer ${API_KEY}`,
34 | 'Accept': 'application/json',
35 | 'Content-Type': 'application/json',
36 | },
37 | }
38 | );
39 |
40 | const currentServerDetails = serverDetailsResponse.data.attributes;
41 |
42 | // Prepare the update payload
43 | const updatePayload = {
44 | startup: startup || currentServerDetails.container.startup_command,
45 | environment: environment || currentServerDetails.container.environment,
46 | egg: egg || currentServerDetails.egg,
47 | image: image || currentServerDetails.container.image,
48 | skip_scripts: skip_scripts !== undefined ? skip_scripts : false,
49 | };
50 |
51 | // Send the update request
52 | const response = await axios.patch(
53 | `${PANEL_URL}/api/application/servers/${serverId}/startup`,
54 | updatePayload,
55 | {
56 | headers: {
57 | 'Authorization': `Bearer ${API_KEY}`,
58 | 'Accept': 'application/json',
59 | 'Content-Type': 'application/json',
60 | },
61 | }
62 | );
63 |
64 | res.json(response.data);
65 | } catch (error) {
66 | console.error('Error updating server startup:', error);
67 | res.status(500).json({ error: 'Internal server error' });
68 | }
69 | });
70 |
71 | app.use("/api", router);
72 | };
--------------------------------------------------------------------------------
/modules/server:users.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:users */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:users",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // GET /api/server/:id/users - List users
23 | router.get('/server/:id/users', isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.id;
26 | const response = await axios.get(
27 | `${PANEL_URL}/api/client/servers/${serverId}/users`,
28 | {
29 | headers: {
30 | 'Authorization': `Bearer ${API_KEY}`,
31 | 'Accept': 'application/json',
32 | 'Content-Type': 'application/json',
33 | },
34 | }
35 | );
36 |
37 | await updateSubuserInfo(serverId, req.session.userinfo.id);
38 |
39 | res.json(response.data);
40 | } catch (error) {
41 | console.error('Error fetching users:', error);
42 | res.status(500).json({ error: 'Internal server error' });
43 | }
44 | });
45 |
46 | // POST /api/server/:id/users - Create user
47 | router.post('/server/:id/users', isAuthenticated, ownsServer, async (req, res) => {
48 | try {
49 | const serverId = req.params.id;
50 | const { email } = req.body;
51 |
52 | if (!email) {
53 | return res.status(400).json({ error: 'Email is required' });
54 | }
55 |
56 | const response = await axios.post(
57 | `${PANEL_URL}/api/client/servers/${serverId}/users`,
58 | {
59 | email,
60 | permissions: [
61 | "control.console", "control.start", "control.stop", "control.restart",
62 | "user.create", "user.read", "user.update", "user.delete",
63 | "file.create", "file.read", "file.update", "file.delete",
64 | "file.archive", "file.sftp", "backup.create", "backup.read",
65 | "backup.delete", "backup.update", "backup.download",
66 | "allocation.update", "startup.update", "startup.read",
67 | "database.create", "database.read", "database.update",
68 | "database.delete", "database.view_password", "schedule.create",
69 | "schedule.read", "schedule.update", "settings.rename",
70 | "schedule.delete", "settings.reinstall", "websocket.connect"
71 | ]
72 | },
73 | {
74 | headers: {
75 | 'Authorization': `Bearer ${API_KEY}`,
76 | 'Accept': 'application/json',
77 | 'Content-Type': 'application/json',
78 | },
79 | }
80 | );
81 |
82 | await updateSubuserInfo(serverId, req.session.userinfo.id);
83 | await addUserToAllUsersList(response.data.attributes.username);
84 |
85 | res.status(201).json(response.data);
86 | } catch (error) {
87 | console.error('Error creating user:', error);
88 | res.status(500).json({ error: 'Internal server error' });
89 | }
90 | });
91 |
92 | // DELETE /api/server/:id/users/:userId - Delete user
93 | router.delete('/server/:id/users/:userId', isAuthenticated, ownsServer, async (req, res) => {
94 | try {
95 | const { id: serverId, userId } = req.params;
96 | await axios.delete(
97 | `${PANEL_URL}/api/client/servers/${serverId}/users/${userId}`,
98 | {
99 | headers: {
100 | 'Authorization': `Bearer ${API_KEY}`,
101 | 'Accept': 'application/json',
102 | },
103 | }
104 | );
105 | res.status(204).send();
106 | } catch (error) {
107 | console.error('Error deleting user:', error);
108 | res.status(500).json({ error: 'Internal server error' });
109 | }
110 | });
111 |
112 | // Helper functions
113 | async function updateSubuserInfo(serverId, serverOwnerId) {
114 | try {
115 | const response = await axios.get(
116 | `${PANEL_URL}/api/client/servers/${serverId}/users`,
117 | {
118 | headers: {
119 | 'Authorization': `Bearer ${API_KEY}`,
120 | 'Accept': 'application/json',
121 | },
122 | }
123 | );
124 |
125 | const subusers = response.data.data.map(user => ({
126 | id: user.attributes.username,
127 | username: user.attributes.username,
128 | email: user.attributes.email,
129 | }));
130 |
131 | await db.set(`subusers-${serverId}`, subusers);
132 |
133 | const serverName = await getServerName(serverId);
134 | for (const subuser of subusers) {
135 | let subuserServers = await db.get(`subuser-servers-${subuser.id}`) || [];
136 | if (!subuserServers.some(server => server.id === serverId)) {
137 | subuserServers.push({
138 | id: serverId,
139 | name: serverName,
140 | ownerId: serverOwnerId
141 | });
142 | await db.set(`subuser-servers-${subuser.id}`, subuserServers);
143 | }
144 | }
145 | } catch (error) {
146 | console.error(`Error updating subuser info:`, error);
147 | }
148 | }
149 |
150 | async function getServerName(serverId) {
151 | try {
152 | const response = await axios.get(
153 | `${PANEL_URL}/api/client/servers/${serverId}`,
154 | {
155 | headers: {
156 | 'Authorization': `Bearer ${API_KEY}`,
157 | 'Accept': 'application/json',
158 | },
159 | }
160 | );
161 | return response.data.attributes.name;
162 | } catch (error) {
163 | return 'Unknown Server';
164 | }
165 | }
166 |
167 | async function addUserToAllUsersList(userId) {
168 | let allUsers = await db.get('all_users') || [];
169 | if (!allUsers.includes(userId)) {
170 | allUsers.push(userId);
171 | await db.set('all_users', allUsers);
172 | }
173 | }
174 |
175 | app.use("/api", router);
176 | };
--------------------------------------------------------------------------------
/modules/server:users_legacy.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:users_legacy */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const { isAuthenticated } = require("./server:core.js");
7 |
8 | /* --------------------------------------------- */
9 | /* Prism Module */
10 | /* --------------------------------------------- */
11 | const PrismModule = {
12 | name: "server:users_legacy",
13 | api_level: 3,
14 | target_platform: "0.5.0",
15 | };
16 |
17 | module.exports.PrismModule = PrismModule;
18 | module.exports.load = async function (app, db) {
19 | const router = express.Router();
20 |
21 | // GET /api/subuser-servers - List servers where user is a subuser
22 | router.get('/subuser-servers', isAuthenticated, async (req, res) => {
23 | try {
24 | const userId = req.session.pterodactyl.username;
25 | let subuserServers = await db.get(`subuser-servers-${userId}`) || [];
26 | res.json(subuserServers);
27 | } catch (error) {
28 | console.error('Error fetching subuser servers:', error);
29 | res.status(500).json({ error: 'Internal server error' });
30 | }
31 | });
32 |
33 | // POST /api/sync-user-servers - Sync user's servers and subuser permissions
34 | router.post('/subuser-servers-sync', isAuthenticated, async (req, res) => {
35 | try {
36 | const userId = req.session.pterodactyl.id;
37 |
38 | // Add the current user to the all_users list
39 | await addUserToAllUsersList(userId);
40 |
41 | // Sync owned servers
42 | const ownedServers = req.session.pterodactyl.relationships.servers.data;
43 | for (const server of ownedServers) {
44 | await updateSubuserInfo(server.attributes.identifier, userId);
45 | }
46 |
47 | // Fetch and sync subuser servers
48 | const subuserServers = await db.get(`subuser-servers-${userId}`) || [];
49 | for (const server of subuserServers) {
50 | await updateSubuserInfo(server.id, server.ownerId);
51 | }
52 |
53 | res.json({ message: 'User servers synced successfully' });
54 | } catch (error) {
55 | console.error('Error syncing user servers:', error);
56 | res.status(500).json({ error: 'Internal server error' });
57 | }
58 | });
59 |
60 | async function addUserToAllUsersList(userId) {
61 | let allUsers = await db.get('all_users') || [];
62 | if (!allUsers.includes(userId)) {
63 | allUsers.push(userId);
64 | await db.set('all_users', allUsers);
65 | }
66 | }
67 |
68 | app.use("/api", router);
69 | };
--------------------------------------------------------------------------------
/modules/server:variables.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:variables */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:variables",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // GET /api/server/:id/variables - Get server variables
23 | router.get('/server/:id/variables', isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.id;
26 | const response = await axios.get(
27 | `${PANEL_URL}/api/client/servers/${serverId}/startup`,
28 | {
29 | headers: {
30 | Authorization: `Bearer ${API_KEY}`,
31 | Accept: 'application/json',
32 | },
33 | }
34 | );
35 | res.json(response.data);
36 | } catch (error) {
37 | console.error('Error fetching server variables:', error);
38 | res.status(500).json({ error: 'Internal server error' });
39 | }
40 | });
41 |
42 | // PUT /api/server/:id/variables - Update server variable
43 | router.put('/server/:id/variables', isAuthenticated, ownsServer, async (req, res) => {
44 | try {
45 | const serverId = req.params.id;
46 | const { key, value } = req.body;
47 |
48 | if (!key || value === undefined) {
49 | return res.status(400).json({ error: 'Missing key or value' });
50 | }
51 |
52 | const response = await axios.put(
53 | `${PANEL_URL}/api/client/servers/${serverId}/startup/variable`,
54 | { key, value },
55 | {
56 | headers: {
57 | Authorization: `Bearer ${API_KEY}`,
58 | Accept: 'application/json',
59 | 'Content-Type': 'application/json',
60 | },
61 | }
62 | );
63 | res.json(response.data);
64 | } catch (error) {
65 | console.error('Error updating server variable:', error);
66 | res.status(500).json({ error: 'Internal server error' });
67 | }
68 | });
69 |
70 | app.use("/api", router);
71 | };
--------------------------------------------------------------------------------
/modules/server:websocket.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:websocket */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:websocket",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // GET /api/server/:id/websocket - Get WebSocket credentials
23 | router.get("/server/:id/websocket", isAuthenticated, ownsServer, async (req, res) => {
24 | try {
25 | const serverId = req.params.id;
26 | const response = await axios.get(
27 | `${PANEL_URL}/api/client/servers/${serverId}/websocket`,
28 | {
29 | headers: {
30 | Authorization: `Bearer ${API_KEY}`,
31 | Accept: "application/json",
32 | "Content-Type": "application/json",
33 | },
34 | }
35 | );
36 |
37 | // Return the WebSocket credentials to the client
38 | res.json(response.data);
39 | } catch (error) {
40 | console.error("Error fetching WebSocket credentials:", error);
41 | res.status(500).json({ error: "Internal server error" });
42 | }
43 | });
44 |
45 | // GET /api/server/:id - Get server details (needed for console)
46 | router.get("/server/:id", isAuthenticated, ownsServer, async (req, res) => {
47 | try {
48 | const serverId = req.params.id;
49 | const response = await axios.get(
50 | `${PANEL_URL}/api/client/servers/${serverId}`,
51 | {
52 | headers: {
53 | Authorization: `Bearer ${API_KEY}`,
54 | Accept: "application/json",
55 | "Content-Type": "application/json",
56 | },
57 | }
58 | );
59 | res.json(response.data);
60 | } catch (error) {
61 | console.error("Error fetching server details:", error);
62 | res.status(500).json({ error: "Internal server error" });
63 | }
64 | });
65 |
66 | app.use("/api", router);
67 | };
--------------------------------------------------------------------------------
/modules/server:workflow.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:workflow */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const schedule = require("node-schedule");
7 | const { isAuthenticated, ownsServer, logActivity, workflowsFilePath } = require("./server:core.js");
8 | const fs = require("fs");
9 |
10 | /* --------------------------------------------- */
11 | /* Prism Module */
12 | /* --------------------------------------------- */
13 | const PrismModule = {
14 | name: "server:workflow",
15 | api_level: 3,
16 | target_platform: "0.5.0",
17 | };
18 |
19 | module.exports.PrismModule = PrismModule;
20 | module.exports.load = async function (app, db) {
21 | const router = express.Router();
22 |
23 | function saveWorkflowToFile(instanceId, workflow) {
24 | try {
25 | let workflows = {};
26 | if (fs.existsSync(workflowsFilePath)) {
27 | const data = fs.readFileSync(workflowsFilePath, "utf8");
28 | workflows = JSON.parse(data);
29 | }
30 | workflows[instanceId] = workflow;
31 | fs.writeFileSync(workflowsFilePath, JSON.stringify(workflows, null, 2), "utf8");
32 | } catch (error) {
33 | console.error("Error saving workflow to file:", error);
34 | }
35 | }
36 |
37 | function loadWorkflowFromFile(instanceId) {
38 | try {
39 | if (fs.existsSync(workflowsFilePath)) {
40 | const data = fs.readFileSync(workflowsFilePath, "utf8");
41 | const workflows = JSON.parse(data);
42 | return workflows[instanceId] || null;
43 | }
44 | return null;
45 | } catch (error) {
46 | console.error("Error loading workflow from file:", error);
47 | return null;
48 | }
49 | }
50 |
51 | // GET workflow
52 | router.get("/server/:id/workflow", isAuthenticated, ownsServer, async (req, res) => {
53 | try {
54 | const serverId = req.params.id;
55 | let workflow = await db.get(serverId + "_workflow");
56 | if (!workflow) {
57 | workflow = loadWorkflowFromFile(serverId);
58 | }
59 |
60 | if (!workflow) {
61 | workflow = {};
62 | }
63 |
64 | res.json(workflow);
65 | } catch (error) {
66 | console.error("Error fetching workflow:", error);
67 | res.status(500).json({ error: "Internal server error" });
68 | }
69 | });
70 |
71 | // POST save workflow
72 | router.post("/server/:instanceId/workflow/save-workflow", isAuthenticated, ownsServer, async (req, res) => {
73 | const { instanceId } = req.params;
74 | const workflow = req.body;
75 |
76 | if (!instanceId || !workflow) {
77 | return res.status(400).json({ success: false, message: "Missing required data" });
78 | }
79 |
80 | try {
81 | const scheduledJob = schedule.scheduledJobs[`job_${instanceId}`];
82 | if (scheduledJob) {
83 | scheduledJob.cancel();
84 | }
85 |
86 | await db.set(instanceId + "_workflow", workflow);
87 | saveWorkflowToFile(instanceId, workflow);
88 |
89 | scheduleWorkflowExecution(instanceId, workflow);
90 | saveScheduledWorkflows();
91 |
92 | await logActivity(db, instanceId, 'Save Workflow', { workflowDetails: workflow });
93 |
94 | res.json({ success: true, message: "Workflow saved successfully" });
95 | } catch (error) {
96 | console.error("Error saving workflow:", error);
97 | res.status(500).json({ success: false, message: "Internal server error" });
98 | }
99 | });
100 |
101 | function scheduleWorkflowExecution(instanceId, workflow) {
102 | const blocks = workflow.blocks;
103 | const intervalBlock = blocks.find((block) => block.type === "interval");
104 |
105 | if (intervalBlock) {
106 | const intervalMinutes = parseInt(intervalBlock.meta.selectedValue, 10);
107 | const rule = new schedule.RecurrenceRule();
108 | rule.minute = new schedule.Range(0, 59, intervalMinutes);
109 |
110 | schedule.scheduleJob(`job_${instanceId}`, rule, () => {
111 | executeWorkflow(instanceId);
112 | });
113 | }
114 | }
115 |
116 | function executeWorkflow(instanceId) {
117 | const workflow = loadWorkflowFromFile(instanceId);
118 | if (workflow) {
119 | const blocks = workflow.blocks;
120 | blocks.filter((block) => block.type === "power").forEach((block) => {
121 | executePowerAction(instanceId, block.meta.selectedValue);
122 | });
123 | }
124 | }
125 |
126 | function saveScheduledWorkflows() {
127 | try {
128 | const scheduledWorkflows = {};
129 | for (const job of Object.values(schedule.scheduledJobs)) {
130 | if (job.name.startsWith("job_")) {
131 | const instanceId = job.name.split("_")[1];
132 | scheduledWorkflows[instanceId] = job.nextInvocation();
133 | }
134 | }
135 | const scheduledWorkflowsFilePath = path.join(__dirname, "../storage/scheduledWorkflows.json");
136 | fs.writeFileSync(scheduledWorkflowsFilePath, JSON.stringify(scheduledWorkflows, null, 2), "utf8");
137 | } catch (error) {
138 | console.error("Error saving scheduled workflows:", error);
139 | }
140 | }
141 |
142 | app.use("/api", router);
143 | };
--------------------------------------------------------------------------------
/modules/server:worlds.js:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------- */
2 | /* server:worlds */
3 | /* --------------------------------------------- */
4 |
5 | const express = require("express");
6 | const axios = require("axios");
7 | const { isAuthenticated, ownsServer, logActivity, PANEL_URL, API_KEY } = require("./server:core.js");
8 |
9 | /* --------------------------------------------- */
10 | /* Prism Module */
11 | /* --------------------------------------------- */
12 | const PrismModule = {
13 | name: "server:worlds",
14 | api_level: 3,
15 | target_platform: "0.5.0",
16 | };
17 |
18 | module.exports.PrismModule = PrismModule;
19 | module.exports.load = async function (app, db) {
20 | const router = express.Router();
21 |
22 | // Helper function to get world type
23 | function getWorldType(worldName, defaultWorld) {
24 | if (worldName === defaultWorld) return 'default';
25 | if (worldName === `${defaultWorld}_nether`) return 'nether';
26 | if (worldName === `${defaultWorld}_the_end`) return 'end';
27 | return 'custom';
28 | }
29 |
30 | // Helper function to check if directory is a valid world
31 | async function isValidWorld(fileData, serverId) {
32 | try {
33 | if (fileData.attributes.mimetype !== "inode/directory" ||
34 | fileData.attributes.name.startsWith('.')) {
35 | return false;
36 | }
37 |
38 | if (fileData.attributes.name.endsWith('_nether') ||
39 | fileData.attributes.name.endsWith('_the_end')) {
40 | return true;
41 | }
42 |
43 | const worldContents = await axios.get(
44 | `${PANEL_URL}/api/client/servers/${serverId}/files/list`,
45 | {
46 | params: { directory: `/${fileData.attributes.name}` },
47 | headers: {
48 | 'Authorization': `Bearer ${API_KEY}`,
49 | 'Accept': 'application/json',
50 | },
51 | }
52 | );
53 |
54 | return worldContents.data.data.some(file =>
55 | file.attributes.name === 'level.dat' &&
56 | !file.attributes.mimetype.startsWith('inode/')
57 | );
58 | } catch (error) {
59 | console.error(`Error checking if ${fileData.attributes.name} is a valid world:`, error);
60 | return false;
61 | }
62 | }
63 |
64 | // List worlds endpoint
65 | router.get('/server/:id/worlds', isAuthenticated, ownsServer, async (req, res) => {
66 | try {
67 | const serverId = req.params.id;
68 |
69 | // Get server.properties to find level-name
70 | const serverPropsResponse = await axios.get(
71 | `${PANEL_URL}/api/client/servers/${serverId}/files/contents`,
72 | {
73 | params: { file: '/server.properties' },
74 | headers: {
75 | 'Authorization': `Bearer ${API_KEY}`,
76 | 'Accept': 'application/json',
77 | },
78 | }
79 | );
80 |
81 | const serverProps = serverPropsResponse.data
82 | .split('\n')
83 | .reduce((acc, line) => {
84 | const [key, value] = line.split('=');
85 | if (key && value) acc[key.trim()] = value.trim();
86 | return acc;
87 | }, {});
88 |
89 | const defaultWorld = serverProps['level-name'] || 'world';
90 |
91 | // List contents of root directory
92 | const response = await axios.get(
93 | `${PANEL_URL}/api/client/servers/${serverId}/files/list`,
94 | {
95 | headers: {
96 | 'Authorization': `Bearer ${API_KEY}`,
97 | 'Accept': 'application/json',
98 | },
99 | }
100 | );
101 |
102 | // Filter for world folders
103 | const fl = await Promise.all(
104 | response.data.data.map(async (folder) => {
105 | const isWorld = await isValidWorld(folder, serverId);
106 | return isWorld ? folder : null;
107 | })
108 | );
109 |
110 | const worldFolders = fl.filter(folder => folder !== null);
111 |
112 | // Get tracked custom worlds from database
113 | const trackedWorlds = await db.get(`worlds-${serverId}`) || [];
114 | const trackedWorldNames = new Set(trackedWorlds);
115 |
116 | // Categorize worlds
117 | const worlds = {
118 | default: null,
119 | nether: null,
120 | end: null,
121 | custom: []
122 | };
123 |
124 | for (const folder of worldFolders) {
125 | const worldName = folder.attributes.name;
126 | const worldType = getWorldType(worldName, defaultWorld);
127 |
128 | const worldData = {
129 | attributes: {
130 | ...folder.attributes,
131 | type: worldType,
132 | isCustom: trackedWorldNames.has(worldName)
133 | }
134 | };
135 |
136 | if (worldType === 'custom') {
137 | worlds.custom.push(worldData);
138 | } else {
139 | worlds[worldType] = worldData;
140 | }
141 | }
142 |
143 | res.json(worlds);
144 | } catch (error) {
145 | console.error('Error listing worlds:', error);
146 | res.status(500).json({ error: 'Internal server error' });
147 | }
148 | });
149 |
150 | // Delete world endpoint
151 | router.delete('/server/:id/worlds/:worldName', isAuthenticated, ownsServer, async (req, res) => {
152 | try {
153 | const { id: serverId, worldName } = req.params;
154 |
155 | // Get server.properties to check if trying to delete default world
156 | const serverPropsResponse = await axios.get(
157 | `${PANEL_URL}/api/client/servers/${serverId}/files/contents`,
158 | {
159 | params: { file: '/server.properties' },
160 | headers: {
161 | 'Authorization': `Bearer ${API_KEY}`,
162 | 'Accept': 'application/json',
163 | },
164 | }
165 | );
166 |
167 | const serverProps = serverPropsResponse.data
168 | .split('\n')
169 | .reduce((acc, line) => {
170 | const [key, value] = line.split('=');
171 | if (key && value) acc[key.trim()] = value.trim();
172 | return acc;
173 | }, {});
174 |
175 | const defaultWorld = serverProps['level-name'] || 'world';
176 |
177 | // Prevent deletion of default world and its dimensions
178 | if (worldName === defaultWorld ||
179 | worldName === `${defaultWorld}_nether` ||
180 | worldName === `${defaultWorld}_the_end`) {
181 | return res.status(400).json({ error: 'Cannot delete default world or its dimensions' });
182 | }
183 |
184 | // Delete the world folder
185 | await axios.post(
186 | `${PANEL_URL}/api/client/servers/${serverId}/files/delete`,
187 | {
188 | root: '/',
189 | files: [worldName]
190 | },
191 | {
192 | headers: {
193 | 'Authorization': `Bearer ${API_KEY}`,
194 | 'Accept': 'application/json',
195 | },
196 | }
197 | );
198 |
199 | // Remove from tracked worlds
200 | const trackedWorlds = await db.get(`worlds-${serverId}`) || [];
201 | const updatedWorlds = trackedWorlds.filter(w => w !== worldName);
202 | await db.set(`worlds-${serverId}`, updatedWorlds);
203 |
204 | await logActivity(db, serverId, 'Delete World', { worldName });
205 | res.json({ success: true });
206 | } catch (error) {
207 | console.error('Error deleting world:', error);
208 | res.status(500).json({ error: 'Internal server error' });
209 | }
210 | });
211 |
212 | app.use("/api", router);
213 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prism",
3 | "version": "0.5.0",
4 | "description": "The official Heliactyl client area for the Pterodactyl Panel.",
5 | "main": "app.js",
6 | "author": "Matt James",
7 | "dependencies": {
8 | "@iarna/toml": "^2.2.5",
9 | "@keyv/sqlite": "^3.6.7",
10 | "@tailwindcss/forms": "^0.5.7",
11 | "apicache": "^1.6.3",
12 | "axios": "^1.7.7",
13 | "bcrypt": "^5.1.1",
14 | "chalk": "^4.1.0",
15 | "chokidar": "^3.6.0",
16 | "cli-progress": "^3.12.0",
17 | "compression": "^1.7.5",
18 | "connect-redis": "^7.1.1",
19 | "connect-sqlite3": "^0.9.15",
20 | "cookie": "^1.0.1",
21 | "cookie-parser": "^1.4.6",
22 | "ejs": "^3.1.7",
23 | "express": "^4.21.2",
24 | "express-openid-connect": "^2.17.1",
25 | "express-rate-limit": "^7.4.1",
26 | "express-session": "^1.17.2",
27 | "express-slow-down": "^1.6.0",
28 | "express-validator": "^7.2.0",
29 | "express-ws": "^4.0.0",
30 | "form-data": "^4.0.1",
31 | "googleapis": "^144.0.0",
32 | "helmet": "^8.0.0",
33 | "hono": "^4.6.14",
34 | "ioredis": "^5.4.1",
35 | "javascript-obfuscator": "^4.0.2",
36 | "jsonwebtoken": "^9.0.2",
37 | "keyv": "^4.5.4",
38 | "lru-cache": "^7.18.3",
39 | "moment": "^2.30.1",
40 | "mysql2": "^3.11.4",
41 | "nocache": "^4.0.0",
42 | "node-cache": "^5.1.2",
43 | "node-fetch": "^2.6.6",
44 | "node-schedule": "^2.1.1",
45 | "ora": "^5.4.1",
46 | "passport": "^0.7.0",
47 | "passport-auth0": "^1.4.4",
48 | "preline": "^2.3.0",
49 | "redis": "^4.7.0",
50 | "sqlite3": "^5.1.7",
51 | "ssh2-sftp-client": "^11.0.0",
52 | "sticky-session": "^1.1.2",
53 | "swagger-jsdoc": "^6.2.8",
54 | "swagger-ui-express": "^5.0.1",
55 | "url": "^0.11.4",
56 | "uuid": "^10.0.0",
57 | "winston": "^3.14.2",
58 | "ws": "^8.18.0",
59 | "xml2js": "^0.6.2",
60 | "yamljs": "^0.3.0"
61 | },
62 | "scripts": {
63 | "mono:build": "cd panel && npm run build",
64 | "db:build:macos": "cd prismdb && cargo build --release && cp target/release/libprismdb.dylib ../ffi/libprismdb.dylib",
65 | "db:build:linux": "cd prismdb && cargo build --release && cp target/release/libprismdb.so ../ffi/libprismdb.so",
66 | "db:build:windows": "cd prismdb && cargo build --release && cp target/release/prismdb.dll ../ffi/prismdb.dll",
67 | "start": "node --no-deprecation app.js",
68 | "build": "npx tailwindcss -i ./assets/tw.conf -o ./assets/tailwind.css --watch"
69 | },
70 | "devDependencies": {
71 | "@babel/cli": "^7.23.9",
72 | "@babel/core": "^7.24.0",
73 | "@babel/plugin-transform-modules-commonjs": "^7.23.3",
74 | "@babel/preset-env": "^7.24.0"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | const colors = require('tailwindcss/colors')
3 |
4 | module.exports = {
5 | content: ["./views/**/*.ejs"],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [
10 | require('@tailwindcss/forms'),
11 | ],
12 | }
--------------------------------------------------------------------------------
|