zyket 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/src/extensions/bullboard/index.js +3 -2
- package/src/services/vite/index.js +42 -0
- package/src/templates/default/frontend/main.jsx +12 -3
- package/src/templates/default/frontend/src/hooks/useAuth.jsx +51 -0
- package/src/templates/default/frontend/src/hooks/useLayout.jsx +18 -0
- package/src/templates/default/frontend/src/layouts/auth/index.jsx +45 -0
- package/src/templates/default/frontend/src/layouts/auth/routes.js +18 -0
- package/src/templates/default/frontend/src/layouts/landing/index.jsx +60 -0
- package/src/templates/default/frontend/src/layouts/landing/routes.js +10 -0
- package/src/templates/default/frontend/src/layouts/panel/index.jsx +116 -0
- package/src/templates/default/frontend/src/layouts/panel/routes.js +11 -0
- package/src/templates/default/frontend/src/middlewares/AdminMiddleware.jsx +17 -0
- package/src/templates/default/frontend/src/middlewares/LoggedMiddleware.jsx +21 -0
- package/src/templates/default/frontend/src/middlewares/NotLoggedMiddleware.jsx +15 -0
- package/src/templates/default/frontend/src/store/index.jsx +5 -0
- package/src/templates/default/frontend/src/store/storeAuth.jsx +14 -0
- package/src/templates/default/frontend/src/views/auth/index.jsx +5 -0
- package/src/templates/default/frontend/src/views/auth/register/index.jsx +5 -0
- package/src/templates/default/frontend/src/views/landing/index.jsx +5 -0
- package/src/templates/default/frontend/src/views/panel/dashboard/index.jsx +5 -0
- package/src/utils/EnvManager.js +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zyket",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
"node-dependency-injection": "^3.2.6",
|
|
32
32
|
"pg": "^8.20.0",
|
|
33
33
|
"prompts": "^2.4.2",
|
|
34
|
+
"prop-types": "^15.8.1",
|
|
34
35
|
"react": "^19.2.4",
|
|
35
36
|
"react-dom": "^19.2.4",
|
|
37
|
+
"react-router-dom": "^7.14.0",
|
|
36
38
|
"redis": "^5.11.0",
|
|
37
39
|
"sequelize": "^6.37.8",
|
|
38
40
|
"socket.io": "^4.8.3",
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
"swagger-ui-express": "^5.0.1",
|
|
42
44
|
"tailwindcss": "^4.2.2",
|
|
43
45
|
"umzug": "^3.8.2",
|
|
44
|
-
"vite": "^8.0.2"
|
|
46
|
+
"vite": "^8.0.2",
|
|
47
|
+
"zustand": "^5.0.12"
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -7,16 +7,17 @@ const basicAuth = require('express-basic-auth')
|
|
|
7
7
|
module.exports = class BullBoardExtension extends Extension {
|
|
8
8
|
path;
|
|
9
9
|
|
|
10
|
-
constructor({ path = '/bullboard' } = {}) {
|
|
10
|
+
constructor({ path = '/bullboard', basePath = '' } = {}) {
|
|
11
11
|
super("BullBoardExtension");
|
|
12
12
|
this.path = path || '/bullboard';
|
|
13
|
+
this.basePath = basePath;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
load(container) {
|
|
16
17
|
if (!container.get('bullmq')) return container.get('logger').warn('BullBoardExtension: bullmq service not found, skipping BullBoard setup');
|
|
17
18
|
const bull = container.get('bullmq')
|
|
18
19
|
const serverAdapter = new ExpressAdapter()
|
|
19
|
-
serverAdapter.setBasePath(this.path)
|
|
20
|
+
serverAdapter.setBasePath(this.basePath + this.path)
|
|
20
21
|
|
|
21
22
|
createBullBoard({
|
|
22
23
|
queues: Object.values(bull.queues).map(queue => new BullMQAdapter(queue)),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const Service = require("../Service");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
|
+
const EnvManager = require("../../utils/EnvManager");
|
|
4
5
|
|
|
5
6
|
module.exports = class Vite extends Service {
|
|
6
7
|
#container;
|
|
@@ -68,6 +69,47 @@ module.exports = class Vite extends Service {
|
|
|
68
69
|
if (!fs.existsSync(path.join(viteRoot, "styles.css"))) {
|
|
69
70
|
this.#container.get('template-manager').installFile('default/frontend/styles', path.join(process.cwd(), process.env.VITE_ROOT || "frontend", "styles.css"));
|
|
70
71
|
}
|
|
72
|
+
|
|
73
|
+
// Install src directory if it doesn't exist
|
|
74
|
+
if (!fs.existsSync(path.join(viteRoot, "src"))) {
|
|
75
|
+
this.#installSrcFiles(viteRoot);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#installSrcFiles(viteRoot) {
|
|
80
|
+
const templateManager = this.#container.get('template-manager');
|
|
81
|
+
const srcFiles = Object.keys(templateManager.templates).filter(key =>
|
|
82
|
+
key.startsWith('default/frontend/src/')
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const fileKey of srcFiles) {
|
|
86
|
+
const template = templateManager.templates[fileKey];
|
|
87
|
+
// Extract the path after 'default/frontend/'
|
|
88
|
+
const relativePath = template.route.replace(/^default\/frontend\//, '');
|
|
89
|
+
const targetPath = path.join(viteRoot, relativePath);
|
|
90
|
+
|
|
91
|
+
// Create directory if it doesn't exist
|
|
92
|
+
const targetDir = path.dirname(targetPath);
|
|
93
|
+
if (!fs.existsSync(targetDir)) {
|
|
94
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Write the file
|
|
98
|
+
fs.writeFileSync(targetPath, template.content);
|
|
99
|
+
this.#container.get('logger').info(`Installed template file: ${relativePath}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add VITE_API_BASE to .env file
|
|
103
|
+
this.#addViteApiBaseToEnv();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#addViteApiBaseToEnv() {
|
|
107
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
108
|
+
const added = EnvManager.addEnvVariable(envPath, 'VITE_API_BASE', 'http://localhost:3000');
|
|
109
|
+
|
|
110
|
+
if (added) {
|
|
111
|
+
this.#container.get('logger').info('Added VITE_API_BASE to .env file');
|
|
112
|
+
}
|
|
71
113
|
}
|
|
72
114
|
|
|
73
115
|
server() {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { createRoot } from 'react-dom/client'
|
|
2
|
+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
|
3
|
+
import AuthLayout from './src/layouts/auth'
|
|
4
|
+
import LandingLayout from './src/layouts/landing'
|
|
2
5
|
import './styles.css'
|
|
6
|
+
import PanelLayout from './src/layouts/panel'
|
|
3
7
|
|
|
4
|
-
createRoot(document.getElementById('root')).render(<
|
|
5
|
-
<
|
|
6
|
-
|
|
8
|
+
createRoot(document.getElementById('root')).render(<BrowserRouter>
|
|
9
|
+
<Routes>
|
|
10
|
+
<Route path="/" element={<LandingLayout />} />
|
|
11
|
+
<Route path="/auth/*" element={<AuthLayout />} />
|
|
12
|
+
<Route path="/panel/*" element={<PanelLayout />} />
|
|
13
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
14
|
+
</Routes>
|
|
15
|
+
</BrowserRouter>)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useStoreAuth } from "../store";
|
|
2
|
+
|
|
3
|
+
export default function useAuth() {
|
|
4
|
+
const context = useStoreAuth();
|
|
5
|
+
const sessionContext = context?.client?.useSession();
|
|
6
|
+
|
|
7
|
+
const { error: sessionError, isPending, isRefetching, refetch } = sessionContext || {};
|
|
8
|
+
const { user, session } = sessionContext?.data || {};
|
|
9
|
+
|
|
10
|
+
const login = async (email, password) => {
|
|
11
|
+
const { data, error } = await context.client.signIn.email({ email, password });
|
|
12
|
+
if (error) throw error;
|
|
13
|
+
|
|
14
|
+
return data;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const register = async ({ email, password, name, image }) => {
|
|
18
|
+
const payload = { email, password, name, isPublic: true };
|
|
19
|
+
if (typeof image === "string" && image.trim() !== "") payload.image = image;
|
|
20
|
+
|
|
21
|
+
const { data, error } = await context.client.signUp.email(payload);
|
|
22
|
+
if (error) throw error;
|
|
23
|
+
|
|
24
|
+
return data;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const logout = async () => {
|
|
28
|
+
await context.client.signOut({
|
|
29
|
+
fetchOptions: {
|
|
30
|
+
onSuccess: () => {
|
|
31
|
+
localStorage.removeItem('pn');
|
|
32
|
+
window.location.href = "/auth/login"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...context,
|
|
41
|
+
login,
|
|
42
|
+
register,
|
|
43
|
+
logout,
|
|
44
|
+
error: sessionError,
|
|
45
|
+
isPending,
|
|
46
|
+
isRefetching,
|
|
47
|
+
refetch,
|
|
48
|
+
user,
|
|
49
|
+
session,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Route } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
export default function useLayout(routeList) {
|
|
5
|
+
|
|
6
|
+
const routes = routesJSON.map((route, index) => {
|
|
7
|
+
const middlewares = route.middlewares ? [...route.middlewares].reverse() : [];
|
|
8
|
+
let element = <route.component />;
|
|
9
|
+
middlewares.forEach((middleware) => {
|
|
10
|
+
element = React.createElement(middleware, {}, element);
|
|
11
|
+
});
|
|
12
|
+
return <Route key={index} path={route.path} element={element} />;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
routes,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Navigate, Routes, Route } from "react-router-dom";
|
|
2
|
+
import useLayout from "../../hooks/useLayout";
|
|
3
|
+
import layoutRoutes from "./routes";
|
|
4
|
+
|
|
5
|
+
export default function AuthLayout() {
|
|
6
|
+
const { routes } = useLayout(layoutRoutes)
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="min-h-screen bg-[#080808] text-white flex items-center justify-center relative overflow-hidden">
|
|
10
|
+
<div
|
|
11
|
+
className="absolute inset-0 pointer-events-none"
|
|
12
|
+
style={{
|
|
13
|
+
backgroundImage: `
|
|
14
|
+
linear-gradient(rgba(255,107,0,0.04) 1px, transparent 1px),
|
|
15
|
+
linear-gradient(90deg, rgba(255,107,0,0.04) 1px, transparent 1px)
|
|
16
|
+
`,
|
|
17
|
+
backgroundSize: "48px 48px",
|
|
18
|
+
}}
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
<div className="absolute inset-0 pointer-events-none">
|
|
22
|
+
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_50%_50%,rgba(255,107,0,0.08),transparent_65%)]" />
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div className="relative z-10 w-full max-w-sm px-8 py-10 space-y-8">
|
|
26
|
+
<div className="flex flex-col items-center gap-4">
|
|
27
|
+
<div className="w-11 h-11 bg-orange-500 rounded-md flex items-center justify-center">
|
|
28
|
+
<span className="text-black font-black text-base tracking-tighter">PY</span>
|
|
29
|
+
</div>
|
|
30
|
+
<span className="text-white font-black text-2xl tracking-widest uppercase">Pnyise</span>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<Routes>
|
|
34
|
+
{routes}
|
|
35
|
+
<Route path="*" element={<Navigate to="/auth" replace />} />
|
|
36
|
+
</Routes>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="absolute bottom-6 text-xs text-zinc-700">
|
|
40
|
+
© {new Date().getFullYear()} Zyket
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import AuthView from "../../views/auth";
|
|
2
|
+
import AuthRegisterView from "../../views/auth/register";
|
|
3
|
+
import NotLoggedMiddleware from "../../middlewares/NotLoggedMiddleware";
|
|
4
|
+
|
|
5
|
+
module.exports = [
|
|
6
|
+
{
|
|
7
|
+
name: "Auth",
|
|
8
|
+
path: "/auth",
|
|
9
|
+
component: AuthView,
|
|
10
|
+
middlewares: [NotLoggedMiddleware],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "Auth Register",
|
|
14
|
+
path: "/auth/register",
|
|
15
|
+
component: AuthRegisterView,
|
|
16
|
+
middlewares: [NotLoggedMiddleware],
|
|
17
|
+
}
|
|
18
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Navigate, Routes, Route, Link } from "react-router-dom";
|
|
2
|
+
import useLayout from "../../hooks/useLayout";
|
|
3
|
+
|
|
4
|
+
export default function LandingLayout() {
|
|
5
|
+
const { routes } = useLayout(layoutRoutes)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="min-h-screen bg-[#080808] text-white relative overflow-hidden">
|
|
10
|
+
<div
|
|
11
|
+
className="absolute inset-0 pointer-events-none"
|
|
12
|
+
style={{
|
|
13
|
+
backgroundImage: `
|
|
14
|
+
linear-gradient(rgba(255,107,0,0.04) 1px, transparent 1px),
|
|
15
|
+
linear-gradient(90deg, rgba(255,107,0,0.04) 1px, transparent 1px)
|
|
16
|
+
`,
|
|
17
|
+
backgroundSize: "48px 48px",
|
|
18
|
+
}}
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
<div className="absolute inset-0 pointer-events-none">
|
|
22
|
+
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_50%_50%,rgba(255,107,0,0.08),transparent_65%)]" />
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div className="relative z-10">
|
|
26
|
+
<nav className="flex items-center justify-between px-8 py-6 max-w-7xl mx-auto">
|
|
27
|
+
<div className="flex items-center gap-3">
|
|
28
|
+
<div className="w-10 h-10 bg-orange-500 rounded-md flex items-center justify-center">
|
|
29
|
+
<span className="text-black font-black text-sm tracking-tighter">PY</span>
|
|
30
|
+
</div>
|
|
31
|
+
<span className="text-white font-black text-xl tracking-widest uppercase">Pnyise</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="flex items-center gap-6">
|
|
35
|
+
<Link to="/auth" className="text-sm text-zinc-400 hover:text-white transition-colors">
|
|
36
|
+
Sign In
|
|
37
|
+
</Link>
|
|
38
|
+
<Link
|
|
39
|
+
to="/auth"
|
|
40
|
+
className="text-sm bg-orange-500 hover:bg-orange-600 text-black font-semibold px-4 py-2 rounded-md transition-colors"
|
|
41
|
+
>
|
|
42
|
+
Get Started
|
|
43
|
+
</Link>
|
|
44
|
+
</div>
|
|
45
|
+
</nav>
|
|
46
|
+
|
|
47
|
+
<main className="px-8 py-12">
|
|
48
|
+
<Routes>
|
|
49
|
+
{routes}
|
|
50
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
51
|
+
</Routes>
|
|
52
|
+
</main>
|
|
53
|
+
|
|
54
|
+
<footer className="absolute bottom-0 w-full py-6 text-center text-xs text-zinc-700 border-t border-zinc-900">
|
|
55
|
+
© {new Date().getFullYear()} Zyket. All rights reserved.
|
|
56
|
+
</footer>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Navigate, Routes, Route, Link, useLocation } from "react-router-dom";
|
|
2
|
+
import useLayout from "../../hooks/useLayout";
|
|
3
|
+
import PanelDashboardView from "../../views/panel/dashboard";
|
|
4
|
+
import layoutRoutes from "./routes";
|
|
5
|
+
|
|
6
|
+
export default function PanelLayout() {
|
|
7
|
+
const location = useLocation();
|
|
8
|
+
const { routes } = useLayout();
|
|
9
|
+
|
|
10
|
+
const navItems = [
|
|
11
|
+
{ name: "Dashboard", path: "/panel/dashboard", icon: "📊" },
|
|
12
|
+
{ name: "Settings", path: "/panel/settings", icon: "⚙️" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const isActive = (path) => location.pathname === path;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="min-h-screen bg-[#080808] text-white flex">
|
|
19
|
+
{/* Sidebar */}
|
|
20
|
+
<aside className="w-64 bg-[#0a0a0a] border-r border-zinc-900 flex flex-col">
|
|
21
|
+
{/* Logo */}
|
|
22
|
+
<div className="px-6 py-6 border-b border-zinc-900">
|
|
23
|
+
<div className="flex items-center gap-3">
|
|
24
|
+
<div className="w-10 h-10 bg-orange-500 rounded-md flex items-center justify-center">
|
|
25
|
+
<span className="text-black font-black text-sm tracking-tighter">PY</span>
|
|
26
|
+
</div>
|
|
27
|
+
<span className="text-white font-black text-lg tracking-widest uppercase">Pnyise</span>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{/* Navigation */}
|
|
32
|
+
<nav className="flex-1 px-4 py-6 space-y-2">
|
|
33
|
+
{navItems.map((item) => (
|
|
34
|
+
<Link
|
|
35
|
+
key={item.path}
|
|
36
|
+
to={item.path}
|
|
37
|
+
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
|
38
|
+
isActive(item.path)
|
|
39
|
+
? "bg-orange-500 text-black font-semibold"
|
|
40
|
+
: "text-zinc-400 hover:text-white hover:bg-zinc-900"
|
|
41
|
+
}`}
|
|
42
|
+
>
|
|
43
|
+
<span className="text-lg">{item.icon}</span>
|
|
44
|
+
<span className="text-sm">{item.name}</span>
|
|
45
|
+
</Link>
|
|
46
|
+
))}
|
|
47
|
+
</nav>
|
|
48
|
+
|
|
49
|
+
{/* User Menu */}
|
|
50
|
+
<div className="px-4 py-4 border-t border-zinc-900">
|
|
51
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
52
|
+
<div className="w-9 h-9 bg-zinc-800 rounded-full flex items-center justify-center">
|
|
53
|
+
<span className="text-xs font-semibold">U</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex-1 min-w-0">
|
|
56
|
+
<p className="text-sm font-medium truncate">User</p>
|
|
57
|
+
<p className="text-xs text-zinc-500 truncate">user@example.com</p>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<Link
|
|
61
|
+
to="/auth"
|
|
62
|
+
className="w-full mt-2 px-4 py-2 text-sm text-zinc-400 hover:text-white hover:bg-zinc-900 rounded-lg transition-colors flex items-center justify-center"
|
|
63
|
+
>
|
|
64
|
+
Logout
|
|
65
|
+
</Link>
|
|
66
|
+
</div>
|
|
67
|
+
</aside>
|
|
68
|
+
|
|
69
|
+
{/* Main Content */}
|
|
70
|
+
<div className="flex-1 flex flex-col min-w-0 relative overflow-hidden">
|
|
71
|
+
{/* Background Grid */}
|
|
72
|
+
<div
|
|
73
|
+
className="absolute inset-0 pointer-events-none"
|
|
74
|
+
style={{
|
|
75
|
+
backgroundImage: `
|
|
76
|
+
linear-gradient(rgba(255,107,0,0.04) 1px, transparent 1px),
|
|
77
|
+
linear-gradient(90deg, rgba(255,107,0,0.04) 1px, transparent 1px)
|
|
78
|
+
`,
|
|
79
|
+
backgroundSize: "48px 48px",
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<div className="absolute inset-0 pointer-events-none">
|
|
84
|
+
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_50%_50%,rgba(255,107,0,0.08),transparent_65%)]" />
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Header */}
|
|
88
|
+
<header className="relative z-10 px-8 py-6 border-b border-zinc-900 bg-[#080808]/80 backdrop-blur-sm">
|
|
89
|
+
<div className="flex items-center justify-between">
|
|
90
|
+
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
91
|
+
<div className="flex items-center gap-4">
|
|
92
|
+
<button className="p-2 text-zinc-400 hover:text-white transition-colors">
|
|
93
|
+
🔔
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</header>
|
|
98
|
+
|
|
99
|
+
{/* Main Content Area */}
|
|
100
|
+
<main className="relative z-10 flex-1 overflow-auto">
|
|
101
|
+
<div className="p-8">
|
|
102
|
+
<Routes>
|
|
103
|
+
{routes}
|
|
104
|
+
<Route path="*" element={<Navigate to="/panel/dashboard" replace />} />
|
|
105
|
+
</Routes>
|
|
106
|
+
</div>
|
|
107
|
+
</main>
|
|
108
|
+
|
|
109
|
+
{/* Footer */}
|
|
110
|
+
<footer className="relative z-10 py-4 text-center text-xs text-zinc-700 border-t border-zinc-900">
|
|
111
|
+
© {new Date().getFullYear()} Zyket. All rights reserved.
|
|
112
|
+
</footer>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import PanelDashboardView from "../../views/panel/dashboard";
|
|
2
|
+
import LoggedMiddleware from "../../middlewares/LoggedMiddleware";
|
|
3
|
+
|
|
4
|
+
module.exports = [
|
|
5
|
+
{
|
|
6
|
+
name: "Panel Dashboard",
|
|
7
|
+
path: "/panel/dashboard",
|
|
8
|
+
component: PanelDashboardView,
|
|
9
|
+
middlewares: [LoggedMiddleware],
|
|
10
|
+
}
|
|
11
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { Navigate } from "react-router-dom";
|
|
3
|
+
import useAuth from "../hooks/useAuth";
|
|
4
|
+
|
|
5
|
+
export default function AdminMiddleware({ children }) {
|
|
6
|
+
const { user, isPending } = useAuth();
|
|
7
|
+
|
|
8
|
+
if (isPending) return null;
|
|
9
|
+
if (!user) return <Navigate to="/auth/login" />; // Change this to your desired login route
|
|
10
|
+
if (user.role !== "admin") return <Navigate to="/" />; // Change this to your desired forbidden route
|
|
11
|
+
|
|
12
|
+
return children;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
AdminMiddleware.propTypes = {
|
|
16
|
+
children: PropTypes.node.isRequired,
|
|
17
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { Navigate } from "react-router-dom";
|
|
3
|
+
import useAuth from "../hooks/useAuth";
|
|
4
|
+
|
|
5
|
+
export default function LoggedMiddleware({
|
|
6
|
+
children
|
|
7
|
+
}) {
|
|
8
|
+
const { user, isPending } = useAuth();
|
|
9
|
+
|
|
10
|
+
if (isPending) return null;
|
|
11
|
+
if (!user) return <Navigate to={`/auth/login`} />; // Change this to your desired login route
|
|
12
|
+
|
|
13
|
+
return children;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
LoggedMiddleware.propTypes = {
|
|
17
|
+
children: PropTypes.node.isRequired,
|
|
18
|
+
roles: PropTypes.arrayOf(PropTypes.string),
|
|
19
|
+
loginRoute: PropTypes.string,
|
|
20
|
+
forbiddenRoute: PropTypes.string,
|
|
21
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import PropTypes from 'prop-types';
|
|
2
|
+
import { Navigate } from 'react-router-dom';
|
|
3
|
+
import useAuth from '../hooks/useAuth';
|
|
4
|
+
|
|
5
|
+
export default function NotLoggedMiddleware ({ children }) {
|
|
6
|
+
const { user, isPending } = useAuth();
|
|
7
|
+
|
|
8
|
+
if(isPending) return null;
|
|
9
|
+
if(user) return <Navigate to={`/panel`} />; // Change this to your desired home route for logged in users
|
|
10
|
+
return children;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
NotLoggedMiddleware.propTypes = {
|
|
14
|
+
children: PropTypes.node.isRequired
|
|
15
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { createAuthClient } from "better-auth/react";
|
|
3
|
+
import { adminClient } from "better-auth/client/plugins"
|
|
4
|
+
|
|
5
|
+
export const useStoreAuth = create((set) => ({
|
|
6
|
+
client: createAuthClient({
|
|
7
|
+
plugins: [
|
|
8
|
+
adminClient()
|
|
9
|
+
],
|
|
10
|
+
baseURL: `${import.meta.env.VITE_API_BASE}/api/auth/`,
|
|
11
|
+
}),
|
|
12
|
+
|
|
13
|
+
setClient: (client) => set({ client }),
|
|
14
|
+
}));
|
package/src/utils/EnvManager.js
CHANGED
|
@@ -39,4 +39,25 @@ module.exports = class EnvManager {
|
|
|
39
39
|
return `${acc}${key}=${value}\n`;
|
|
40
40
|
}, "");
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
static addEnvVariable(secretsPath, key, value) {
|
|
44
|
+
// Create env file if it doesn't exist
|
|
45
|
+
if (!fs.existsSync(secretsPath)) {
|
|
46
|
+
this.createEnvFile(secretsPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Read existing content
|
|
50
|
+
let envContent = fs.readFileSync(secretsPath, 'utf-8');
|
|
51
|
+
|
|
52
|
+
// Check if the key already exists
|
|
53
|
+
const keyRegex = new RegExp(`^${key}=.*$`, 'm');
|
|
54
|
+
if (keyRegex.test(envContent)) {
|
|
55
|
+
return false; // Key already exists
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add the new environment variable
|
|
59
|
+
envContent += `${key}=${value}\n`;
|
|
60
|
+
fs.writeFileSync(secretsPath, envContent);
|
|
61
|
+
return true; // Key added successfully
|
|
62
|
+
}
|
|
42
63
|
}
|