@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.5",
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.2",
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
- // Handle root path redirection
22
- if (pathname === "/") {
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
- const token = authHeader?.startsWith("Bearer ")
25
- ? authHeader.slice(7)
26
- : null;
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: "/admin",
44
+ Location: "/login",
34
45
  },
35
46
  });
36
47
  }
37
48
 
49
+ // Token exists - redirect to admin dashboard
38
50
  try {
39
- // Verify token to get user info
40
- const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
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
- // Redirect to dashboard if authenticated
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: "/admin/dashboard",
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: "/admin",
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
- const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
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 '../layouts/AdminLayout.astro';
3
- import { collections } from "../lib/config";
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
- const token = authHeader?.startsWith("Bearer ")
16
- ? authHeader.slice(7)
17
- : null;
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: { "Content-Type": "application/json" },
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: { "Content-Type": "application/json" },
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>