@kyro-cms/admin 0.1.5 → 0.1.6
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyro-cms/admin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Admin dashboard for Kyro CMS",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@astrojs/node": "^9.5.5",
|
|
27
27
|
"@astrojs/react": "^4.2.0",
|
|
28
|
-
"@kyro-cms/core": "^0.1.
|
|
28
|
+
"@kyro-cms/core": "^0.1.6",
|
|
29
29
|
"@tailwindcss/vite": "^4.0.0",
|
|
30
30
|
"astro": "^5.4.0",
|
|
31
31
|
"lucide-react": "^0.475.0",
|
|
@@ -41,4 +41,4 @@
|
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@kyro-cms/core": "^0.1.2"
|
|
43
43
|
}
|
|
44
|
-
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles/main.css";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { title } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<!doctype html>
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="UTF-8" />
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
16
|
+
<title>{title} - Kyro CMS</title>
|
|
17
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
18
|
+
</head>
|
|
19
|
+
<body class="bg-[#eaeff2] antialiased text-[#0b1222]">
|
|
20
|
+
<div class="min-h-screen flex items-center justify-center p-6">
|
|
21
|
+
<div class="w-full">
|
|
22
|
+
<!-- Logo -->
|
|
23
|
+
<div class="text-center mb-8">
|
|
24
|
+
<a href="/" class="inline-block">
|
|
25
|
+
<span class="text-4xl font-black tracking-tighter text-[#0b1222]">KYRO.</span>
|
|
26
|
+
</a>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<slot />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
package/src/middleware.ts
CHANGED
|
@@ -10,6 +10,8 @@ const PUBLIC_PATHS = [
|
|
|
10
10
|
"/api/auth/me",
|
|
11
11
|
"/api/auth/users",
|
|
12
12
|
"/api/health",
|
|
13
|
+
"/login",
|
|
14
|
+
"/register",
|
|
13
15
|
"/favicon.svg",
|
|
14
16
|
];
|
|
15
17
|
|
|
@@ -18,40 +20,70 @@ const PUBLIC_PREFIXES = ["/api/collections/", "/api/auth/"];
|
|
|
18
20
|
export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
|
|
19
21
|
const pathname = new URL(url).pathname;
|
|
20
22
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
+
// Helper to extract token from cookie or header
|
|
24
|
+
const getToken = (): string | null => {
|
|
25
|
+
// Check Authorization header first
|
|
23
26
|
const authHeader = request.headers.get("authorization");
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
28
|
+
return authHeader.slice(7);
|
|
29
|
+
}
|
|
30
|
+
// Check cookie
|
|
31
|
+
const cookies = request.headers.get("cookie") || "";
|
|
32
|
+
const match = cookies.match(/auth_token=([^;]+)/);
|
|
33
|
+
return match ? match[1] : null;
|
|
34
|
+
};
|
|
27
35
|
|
|
36
|
+
const token = getToken();
|
|
37
|
+
|
|
38
|
+
// Handle root path - redirect to admin for authenticated users
|
|
39
|
+
if (pathname === "/") {
|
|
28
40
|
if (!token) {
|
|
29
|
-
// Redirect to admin login if not authenticated
|
|
30
41
|
return new Response(null, {
|
|
31
42
|
status: 302,
|
|
32
43
|
headers: {
|
|
33
|
-
Location: "/
|
|
44
|
+
Location: "/login",
|
|
34
45
|
},
|
|
35
46
|
});
|
|
36
47
|
}
|
|
37
48
|
|
|
49
|
+
// Token exists - redirect to admin dashboard
|
|
38
50
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
jwt.verify(token, JWT_SECRET);
|
|
52
|
+
return new Response(null, {
|
|
53
|
+
status: 302,
|
|
54
|
+
headers: {
|
|
55
|
+
Location: "/admin",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
return new Response(null, {
|
|
60
|
+
status: 302,
|
|
61
|
+
headers: {
|
|
62
|
+
Location: "/login",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
41
67
|
|
|
42
|
-
|
|
68
|
+
// Handle /admin path - main dashboard
|
|
69
|
+
if (pathname === "/admin") {
|
|
70
|
+
if (!token) {
|
|
43
71
|
return new Response(null, {
|
|
44
72
|
status: 302,
|
|
45
73
|
headers: {
|
|
46
|
-
Location: "/
|
|
74
|
+
Location: "/login",
|
|
47
75
|
},
|
|
48
76
|
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
jwt.verify(token, JWT_SECRET);
|
|
81
|
+
return next();
|
|
49
82
|
} catch {
|
|
50
|
-
// Invalid token, redirect to login
|
|
51
83
|
return new Response(null, {
|
|
52
84
|
status: 302,
|
|
53
85
|
headers: {
|
|
54
|
-
Location: "/
|
|
86
|
+
Location: "/login",
|
|
55
87
|
},
|
|
56
88
|
});
|
|
57
89
|
}
|
|
@@ -67,9 +99,6 @@ export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
|
|
|
67
99
|
}
|
|
68
100
|
}
|
|
69
101
|
|
|
70
|
-
const authHeader = request.headers.get("authorization");
|
|
71
|
-
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
72
|
-
|
|
73
102
|
if (!token) {
|
|
74
103
|
return new Response(JSON.stringify({ error: "Authentication required" }), {
|
|
75
104
|
status: 401,
|
|
@@ -78,7 +107,7 @@ export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
|
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
try {
|
|
81
|
-
|
|
110
|
+
jwt.verify(token, JWT_SECRET);
|
|
82
111
|
return next();
|
|
83
112
|
} catch {
|
|
84
113
|
return new Response(JSON.stringify({ error: "Invalid or expired token" }), {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
import AdminLayout from '
|
|
3
|
-
import { collections } from "
|
|
2
|
+
import AdminLayout from '../../layouts/AdminLayout.astro';
|
|
3
|
+
import { collections } from "../../lib/config";
|
|
4
4
|
|
|
5
5
|
const authCollections = ['users', 'roles', 'audit_logs'];
|
|
6
6
|
const authItems = authCollections.map(slug => ({
|
|
@@ -11,10 +11,16 @@ async function getAuthApi() {
|
|
|
11
11
|
|
|
12
12
|
export const POST: APIRoute = async ({ request }) => {
|
|
13
13
|
try {
|
|
14
|
+
// Check Authorization header or cookie for token
|
|
15
|
+
let token: string | null = null;
|
|
14
16
|
const authHeader = request.headers.get("authorization");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
18
|
+
token = authHeader.slice(7);
|
|
19
|
+
} else {
|
|
20
|
+
const cookies = request.headers.get("cookie") || "";
|
|
21
|
+
const match = cookies.match(/auth_token=([^;]+)/);
|
|
22
|
+
token = match ? match[1] : null;
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
if (token) {
|
|
20
26
|
const adapter = await getAuthApi();
|
|
@@ -25,12 +31,18 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
25
31
|
|
|
26
32
|
return new Response(JSON.stringify({ success: true }), {
|
|
27
33
|
status: 200,
|
|
28
|
-
headers: {
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
"Set-Cookie": "auth_token=; path=/; max-age=0",
|
|
37
|
+
},
|
|
29
38
|
});
|
|
30
39
|
} catch {
|
|
31
40
|
return new Response(JSON.stringify({ success: true }), {
|
|
32
41
|
status: 200,
|
|
33
|
-
headers: {
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"Set-Cookie": "auth_token=; path=/; max-age=0",
|
|
45
|
+
},
|
|
34
46
|
});
|
|
35
47
|
}
|
|
36
48
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AuthLayout from '../layouts/AuthLayout.astro';
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<AuthLayout title="Sign In">
|
|
6
|
+
<div class="surface-tile p-8 w-full max-w-md">
|
|
7
|
+
<div class="text-center mb-8">
|
|
8
|
+
<h1 class="text-2xl font-black tracking-tight text-[#0b1222]">Welcome back</h1>
|
|
9
|
+
<p class="text-sm text-[#64748b] mt-2">Sign in to your account</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<form id="login-form" class="space-y-4">
|
|
13
|
+
<div>
|
|
14
|
+
<label for="email" class="block text-sm font-bold text-[#0b1222] mb-2">Email</label>
|
|
15
|
+
<input type="email" id="email" name="email" required
|
|
16
|
+
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#0b1222]/20 focus:border-[#0b1222]"
|
|
17
|
+
placeholder="admin@example.com" />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div>
|
|
21
|
+
<label for="password" class="block text-sm font-bold text-[#0b1222] mb-2">Password</label>
|
|
22
|
+
<input type="password" id="password" name="password" required
|
|
23
|
+
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#0b1222]/20 focus:border-[#0b1222]"
|
|
24
|
+
placeholder="••••••••" />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div id="form-message" class="hidden p-3 rounded-xl text-sm font-bold"></div>
|
|
28
|
+
|
|
29
|
+
<button type="submit" class="w-full py-3 bg-[#0b1222] text-white rounded-xl text-sm font-bold hover:bg-[#1a2332] transition-colors">
|
|
30
|
+
Sign In
|
|
31
|
+
</button>
|
|
32
|
+
</form>
|
|
33
|
+
|
|
34
|
+
<p class="text-center text-sm text-[#64748b] mt-6">
|
|
35
|
+
Don't have an account? <a href="/register" class="font-bold text-[#0b1222] hover:underline">Register</a>
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<script is:inline>
|
|
40
|
+
document.getElementById('login-form')?.addEventListener('submit', async (e) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
const form = e.target;
|
|
43
|
+
const message = document.getElementById('form-message');
|
|
44
|
+
const button = form.querySelector('button[type="submit"]');
|
|
45
|
+
|
|
46
|
+
const email = form.email.value;
|
|
47
|
+
const password = form.password.value;
|
|
48
|
+
|
|
49
|
+
button.disabled = true;
|
|
50
|
+
button.textContent = 'Signing in...';
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch('/api/auth/login', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ email, password })
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
|
|
61
|
+
if (res.ok && data.success) {
|
|
62
|
+
// Set cookie for server-side auth
|
|
63
|
+
document.cookie = `auth_token=${data.token}; path=/; max-age=${60*60*24}; samesite=strict`;
|
|
64
|
+
localStorage.setItem('user', JSON.stringify(data.user));
|
|
65
|
+
message.textContent = 'Success! Redirecting...';
|
|
66
|
+
message.className = 'block p-3 rounded-xl text-sm font-bold bg-green-50 text-green-600';
|
|
67
|
+
setTimeout(() => { window.location.href = '/admin'; }, 500);
|
|
68
|
+
} else {
|
|
69
|
+
message.textContent = data.error || 'Login failed';
|
|
70
|
+
message.className = 'block p-3 rounded-xl text-sm font-bold bg-red-50 text-red-600';
|
|
71
|
+
button.disabled = false;
|
|
72
|
+
button.textContent = 'Sign In';
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
message.textContent = 'Connection error';
|
|
76
|
+
message.className = 'block p-3 rounded-xl text-sm font-bold bg-red-50 text-red-600';
|
|
77
|
+
button.disabled = false;
|
|
78
|
+
button.textContent = 'Sign In';
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
</script>
|
|
82
|
+
</AuthLayout>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AuthLayout from '../layouts/AuthLayout.astro';
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<AuthLayout title="Create Account">
|
|
6
|
+
<div class="surface-tile p-8 w-full max-w-md">
|
|
7
|
+
<div class="text-center mb-8">
|
|
8
|
+
<h1 class="text-2xl font-black tracking-tight text-[#0b1222]">Create your account</h1>
|
|
9
|
+
<p class="text-sm text-[#64748b] mt-2">Get started with Kyro CMS</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<form id="register-form" class="space-y-4">
|
|
13
|
+
<div>
|
|
14
|
+
<label for="email" class="block text-sm font-bold text-[#0b1222] mb-2">Email</label>
|
|
15
|
+
<input type="email" id="email" name="email" required
|
|
16
|
+
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#0b1222]/20 focus:border-[#0b1222]"
|
|
17
|
+
placeholder="admin@example.com" />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div>
|
|
21
|
+
<label for="password" class="block text-sm font-bold text-[#0b1222] mb-2">Password</label>
|
|
22
|
+
<input type="password" id="password" name="password" required minlength="8"
|
|
23
|
+
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#0b1222]/20 focus:border-[#0b1222]"
|
|
24
|
+
placeholder="Minimum 8 characters" />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div>
|
|
28
|
+
<label for="confirmPassword" class="block text-sm font-bold text-[#0b1222] mb-2">Confirm Password</label>
|
|
29
|
+
<input type="password" id="confirmPassword" name="confirmPassword" required minlength="8"
|
|
30
|
+
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#0b1222]/20 focus:border-[#0b1222]"
|
|
31
|
+
placeholder="Confirm your password" />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div id="form-message" class="hidden p-3 rounded-xl text-sm font-bold"></div>
|
|
35
|
+
|
|
36
|
+
<button type="submit" class="w-full py-3 bg-[#0b1222] text-white rounded-xl text-sm font-bold hover:bg-[#1a2332] transition-colors">
|
|
37
|
+
Create Account
|
|
38
|
+
</button>
|
|
39
|
+
</form>
|
|
40
|
+
|
|
41
|
+
<p class="text-center text-sm text-[#64748b] mt-6">
|
|
42
|
+
Already have an account? <a href="/login" class="font-bold text-[#0b1222] hover:underline">Sign in</a>
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<script is:inline>
|
|
47
|
+
document.getElementById('register-form')?.addEventListener('submit', async (e) => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
const form = e.target;
|
|
50
|
+
const message = document.getElementById('form-message');
|
|
51
|
+
const button = form.querySelector('button[type="submit"]');
|
|
52
|
+
|
|
53
|
+
const email = form.email.value;
|
|
54
|
+
const password = form.password.value;
|
|
55
|
+
const confirmPassword = form.confirmPassword.value;
|
|
56
|
+
|
|
57
|
+
if (password !== confirmPassword) {
|
|
58
|
+
message.textContent = 'Passwords do not match';
|
|
59
|
+
message.className = 'block p-3 rounded-xl text-sm font-bold bg-red-50 text-red-600';
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (password.length < 8) {
|
|
64
|
+
message.textContent = 'Password must be at least 8 characters';
|
|
65
|
+
message.className = 'block p-3 rounded-xl text-sm font-bold bg-red-50 text-red-600';
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
button.disabled = true;
|
|
70
|
+
button.textContent = 'Creating account...';
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch('/api/auth/register', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ email, password, confirmPassword })
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
|
|
81
|
+
if (res.ok && data.success) {
|
|
82
|
+
// Set cookie for server-side auth
|
|
83
|
+
document.cookie = `auth_token=${data.token}; path=/; max-age=${60*60*24}; samesite=strict`;
|
|
84
|
+
localStorage.setItem('user', JSON.stringify(data.user));
|
|
85
|
+
message.textContent = data.isFirstUser ? 'Super admin account created!' : 'Account created successfully!';
|
|
86
|
+
message.className = 'block p-3 rounded-xl text-sm font-bold bg-green-50 text-green-600';
|
|
87
|
+
setTimeout(() => { window.location.href = '/admin'; }, 1000);
|
|
88
|
+
} else {
|
|
89
|
+
message.textContent = data.error || 'Registration failed';
|
|
90
|
+
message.className = 'block p-3 rounded-xl text-sm font-bold bg-red-50 text-red-600';
|
|
91
|
+
button.disabled = false;
|
|
92
|
+
button.textContent = 'Create Account';
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
message.textContent = 'Connection error';
|
|
96
|
+
message.className = 'block p-3 rounded-xl text-sm font-bold bg-red-50 text-red-600';
|
|
97
|
+
button.disabled = false;
|
|
98
|
+
button.textContent = 'Create Account';
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
</script>
|
|
102
|
+
</AuthLayout>
|