@kyro-cms/admin 0.1.2

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.
Files changed (102) hide show
  1. package/.astro/content.d.ts +154 -0
  2. package/.astro/settings.json +5 -0
  3. package/.astro/types.d.ts +2 -0
  4. package/astro.config.mjs +28 -0
  5. package/bun.lock +1374 -0
  6. package/dist/client/_astro/AdminLayout.DkDpng53.css +1 -0
  7. package/dist/client/_astro/AutoForm.3eJCmCJp.js +1 -0
  8. package/dist/client/_astro/client.DyczpTbx.js +9 -0
  9. package/dist/client/_astro/index.B02hbnpo.js +1 -0
  10. package/dist/client/fonts/Serotiva-Black.woff2 +0 -0
  11. package/dist/client/fonts/Serotiva-Bold.woff2 +0 -0
  12. package/dist/client/fonts/Serotiva-Medium.woff2 +0 -0
  13. package/dist/client/fonts/Serotiva-Regular.woff2 +0 -0
  14. package/dist/client/fonts/Serotiva-SemiBold.woff2 +0 -0
  15. package/dist/server/chunks/AdminLayout_D-_JeUqC.mjs +26 -0
  16. package/dist/server/chunks/_id__BzI_o0qT.mjs +50 -0
  17. package/dist/server/chunks/_id__Cd-jOuY3.mjs +238 -0
  18. package/dist/server/chunks/_id__DvbD--iR.mjs +992 -0
  19. package/dist/server/chunks/_id__vpVaEo16.mjs +128 -0
  20. package/dist/server/chunks/_virtual_astro_server-island-manifest_CQQ1F5PF.mjs +7 -0
  21. package/dist/server/chunks/_virtual_astro_session-driver_Bk3Q189E.mjs +4 -0
  22. package/dist/server/chunks/astro-component_Dbx3T2Nh.mjs +37 -0
  23. package/dist/server/chunks/audit-logs_DrnUMRvY.mjs +74 -0
  24. package/dist/server/chunks/config_CPXslElD.mjs +4221 -0
  25. package/dist/server/chunks/dataStore_Dl7cA2Qp.mjs +89 -0
  26. package/dist/server/chunks/index_CVqOkerS.mjs +2960 -0
  27. package/dist/server/chunks/index_CX8SQ4BF.mjs +55 -0
  28. package/dist/server/chunks/index_CYofDU51.mjs +58 -0
  29. package/dist/server/chunks/index_DdNRhuaM.mjs +55 -0
  30. package/dist/server/chunks/index_DupPvtIF.mjs +42 -0
  31. package/dist/server/chunks/index_YTS_M-B9.mjs +263 -0
  32. package/dist/server/chunks/index_YeCzuVps.mjs +53 -0
  33. package/dist/server/chunks/login_DLyqMRO8.mjs +93 -0
  34. package/dist/server/chunks/logout_CSbt5wea.mjs +50 -0
  35. package/dist/server/chunks/me_C04jlYhH.mjs +41 -0
  36. package/dist/server/chunks/new_BbQ9b55M.mjs +92 -0
  37. package/dist/server/chunks/node_9bvTewss.mjs +1014 -0
  38. package/dist/server/chunks/noop-entrypoint_BOlrdqWF.mjs +3 -0
  39. package/dist/server/chunks/sequence_9cl7AJy-.mjs +2503 -0
  40. package/dist/server/chunks/server_peBx9VXG.mjs +8117 -0
  41. package/dist/server/chunks/sharp_pmJ7nHES.mjs +142 -0
  42. package/dist/server/chunks/users_Dzddy_YR.mjs +137 -0
  43. package/dist/server/entry.mjs +5 -0
  44. package/dist/server/virtual_astro_middleware.mjs +48 -0
  45. package/package.json +33 -0
  46. package/public/fonts/Serotiva-Black.woff2 +0 -0
  47. package/public/fonts/Serotiva-Bold.woff2 +0 -0
  48. package/public/fonts/Serotiva-Medium.woff2 +0 -0
  49. package/public/fonts/Serotiva-Regular.woff2 +0 -0
  50. package/public/fonts/Serotiva-SemiBold.woff2 +0 -0
  51. package/src/collections/auth/index.ts +155 -0
  52. package/src/components/ActionBar.tsx +215 -0
  53. package/src/components/Admin.tsx +214 -0
  54. package/src/components/AutoForm.tsx +1123 -0
  55. package/src/components/BulkActionsBar.tsx +80 -0
  56. package/src/components/CreateView.tsx +99 -0
  57. package/src/components/DetailView.tsx +329 -0
  58. package/src/components/Icons.tsx +23 -0
  59. package/src/components/ListView.tsx +192 -0
  60. package/src/components/StatusBadge.tsx +76 -0
  61. package/src/components/ThemeProvider.tsx +155 -0
  62. package/src/components/VersionHistoryPanel.tsx +205 -0
  63. package/src/components/fields/CheckboxField.tsx +37 -0
  64. package/src/components/fields/DateField.tsx +42 -0
  65. package/src/components/fields/NumberField.tsx +44 -0
  66. package/src/components/fields/RelationshipField.tsx +87 -0
  67. package/src/components/fields/SelectField.tsx +56 -0
  68. package/src/components/fields/TextField.tsx +49 -0
  69. package/src/components/index.ts +30 -0
  70. package/src/components/layout/Breadcrumbs.tsx +36 -0
  71. package/src/components/layout/Header.tsx +37 -0
  72. package/src/components/layout/Layout.tsx +25 -0
  73. package/src/components/layout/Sidebar.tsx +462 -0
  74. package/src/components/ui/Badge.tsx +14 -0
  75. package/src/components/ui/Button.tsx +41 -0
  76. package/src/components/ui/Dropdown.tsx +82 -0
  77. package/src/components/ui/Modal.tsx +135 -0
  78. package/src/components/ui/SlidePanel.tsx +73 -0
  79. package/src/components/ui/Spinner.tsx +24 -0
  80. package/src/components/ui/Toast.tsx +78 -0
  81. package/src/layouts/AdminLayout.astro +197 -0
  82. package/src/lib/config.ts +68 -0
  83. package/src/lib/dataStore.ts +111 -0
  84. package/src/middleware.ts +48 -0
  85. package/src/pages/[collection]/[id].astro +176 -0
  86. package/src/pages/[collection]/index.astro +180 -0
  87. package/src/pages/api/[collection]/[id].ts +258 -0
  88. package/src/pages/api/[collection]/index.ts +289 -0
  89. package/src/pages/api/auth/[id].ts +142 -0
  90. package/src/pages/api/auth/audit-logs.ts +80 -0
  91. package/src/pages/api/auth/login.ts +101 -0
  92. package/src/pages/api/auth/logout.ts +48 -0
  93. package/src/pages/api/auth/me.ts +36 -0
  94. package/src/pages/api/auth/users.ts +150 -0
  95. package/src/pages/audit/index.astro +110 -0
  96. package/src/pages/index.astro +225 -0
  97. package/src/pages/roles/index.astro +114 -0
  98. package/src/pages/users/[id].astro +174 -0
  99. package/src/pages/users/index.astro +142 -0
  100. package/src/pages/users/new.astro +91 -0
  101. package/src/styles/main.css +1449 -0
  102. package/tsconfig.json +12 -0
@@ -0,0 +1,93 @@
1
+ import { RedisAuthAdapter } from './index_CVqOkerS.mjs';
2
+ import jwt from 'jsonwebtoken';
3
+
4
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
5
+ const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
6
+ async function getAuthApi() {
7
+ return new RedisAuthAdapter({
8
+ url: process.env.REDIS_URL || "redis://localhost:6379",
9
+ tls: process.env.REDIS_TLS === "true"
10
+ });
11
+ }
12
+ const POST = async ({ request }) => {
13
+ try {
14
+ const body = await request.json();
15
+ const { email, password } = body;
16
+ if (!email || !password) {
17
+ return new Response(
18
+ JSON.stringify({ error: "Email and password required" }),
19
+ { status: 400, headers: { "Content-Type": "application/json" } }
20
+ );
21
+ }
22
+ const adapter = await getAuthApi();
23
+ await adapter.connect();
24
+ const user = await adapter.findUserByEmail(email);
25
+ if (!user || !user.passwordHash) {
26
+ await adapter.disconnect();
27
+ return new Response(JSON.stringify({ error: "Invalid credentials" }), {
28
+ status: 401,
29
+ headers: { "Content-Type": "application/json" }
30
+ });
31
+ }
32
+ if (user.locked) {
33
+ await adapter.disconnect();
34
+ return new Response(JSON.stringify({ error: "Account is locked" }), {
35
+ status: 403,
36
+ headers: { "Content-Type": "application/json" }
37
+ });
38
+ }
39
+ const valid = await adapter.verifyPassword(password, user.passwordHash);
40
+ if (!valid) {
41
+ await adapter.recordFailedAttempt(user.id);
42
+ await adapter.disconnect();
43
+ return new Response(JSON.stringify({ error: "Invalid credentials" }), {
44
+ status: 401,
45
+ headers: { "Content-Type": "application/json" }
46
+ });
47
+ }
48
+ await adapter.resetAttempts(user.id);
49
+ const session = await adapter.createSession(user.id, {
50
+ ipAddress: request.headers.get("x-forwarded-for") || "unknown",
51
+ userAgent: request.headers.get("user-agent") || ""
52
+ });
53
+ const token = jwt.sign(
54
+ {
55
+ sub: user.id,
56
+ email: user.email,
57
+ role: user.role,
58
+ tenantId: user.tenantId
59
+ },
60
+ JWT_SECRET,
61
+ { expiresIn: JWT_EXPIRES_IN }
62
+ );
63
+ await adapter.disconnect();
64
+ const { passwordHash, ...safeUser } = user;
65
+ return new Response(
66
+ JSON.stringify({
67
+ success: true,
68
+ user: safeUser,
69
+ token,
70
+ refreshToken: session.refreshToken
71
+ }),
72
+ {
73
+ status: 200,
74
+ headers: { "Content-Type": "application/json" }
75
+ }
76
+ );
77
+ } catch (error) {
78
+ console.error("Login error:", error);
79
+ return new Response(JSON.stringify({ error: "Login failed" }), {
80
+ status: 500,
81
+ headers: { "Content-Type": "application/json" }
82
+ });
83
+ }
84
+ };
85
+
86
+ const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
87
+ __proto__: null,
88
+ POST
89
+ }, Symbol.toStringTag, { value: 'Module' }));
90
+
91
+ const page = () => _page;
92
+
93
+ export { page };
@@ -0,0 +1,50 @@
1
+ import { RedisAuthAdapter } from './index_CVqOkerS.mjs';
2
+
3
+ async function getAuthApi() {
4
+ return new RedisAuthAdapter({
5
+ url: process.env.REDIS_URL || "redis://localhost:6379",
6
+ tls: process.env.REDIS_TLS === "true"
7
+ });
8
+ }
9
+ const POST = async ({ request }) => {
10
+ try {
11
+ const body = await request.json();
12
+ const { refreshToken } = body;
13
+ if (!refreshToken) {
14
+ return new Response(JSON.stringify({ error: "Refresh token required" }), {
15
+ status: 400,
16
+ headers: { "Content-Type": "application/json" }
17
+ });
18
+ }
19
+ const adapter = await getAuthApi();
20
+ await adapter.connect();
21
+ const session = await adapter.findSessionByToken(refreshToken);
22
+ if (!session) {
23
+ await adapter.disconnect();
24
+ return new Response(JSON.stringify({ error: "Invalid refresh token" }), {
25
+ status: 401,
26
+ headers: { "Content-Type": "application/json" }
27
+ });
28
+ }
29
+ await adapter.disconnect();
30
+ return new Response(JSON.stringify({ success: true, session }), {
31
+ status: 200,
32
+ headers: { "Content-Type": "application/json" }
33
+ });
34
+ } catch (error) {
35
+ console.error("Logout error:", error);
36
+ return new Response(JSON.stringify({ error: "Logout failed" }), {
37
+ status: 500,
38
+ headers: { "Content-Type": "application/json" }
39
+ });
40
+ }
41
+ };
42
+
43
+ const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
44
+ __proto__: null,
45
+ POST
46
+ }, Symbol.toStringTag, { value: 'Module' }));
47
+
48
+ const page = () => _page;
49
+
50
+ export { page };
@@ -0,0 +1,41 @@
1
+ import jwt from 'jsonwebtoken';
2
+
3
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
4
+ const GET = async ({ request }) => {
5
+ const authHeader = request.headers.get("authorization");
6
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
7
+ if (!token) {
8
+ return new Response(JSON.stringify({ error: "Not authenticated" }), {
9
+ status: 401,
10
+ headers: { "Content-Type": "application/json" }
11
+ });
12
+ }
13
+ try {
14
+ const payload = jwt.verify(token, JWT_SECRET);
15
+ return new Response(
16
+ JSON.stringify({
17
+ user: {
18
+ id: payload.sub,
19
+ email: payload.email,
20
+ role: payload.role,
21
+ tenantId: payload.tenantId
22
+ }
23
+ }),
24
+ { status: 200, headers: { "Content-Type": "application/json" } }
25
+ );
26
+ } catch {
27
+ return new Response(JSON.stringify({ error: "Invalid token" }), {
28
+ status: 401,
29
+ headers: { "Content-Type": "application/json" }
30
+ });
31
+ }
32
+ };
33
+
34
+ const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
35
+ __proto__: null,
36
+ GET
37
+ }, Symbol.toStringTag, { value: 'Module' }));
38
+
39
+ const page = () => _page;
40
+
41
+ export { page };
@@ -0,0 +1,92 @@
1
+ import { c as createComponent } from './astro-component_Dbx3T2Nh.mjs';
2
+ import 'piccolore';
3
+ import { Q as renderTemplate, a3 as addAttribute, B as maybeRenderHead } from './sequence_9cl7AJy-.mjs';
4
+ import { r as renderComponent } from './server_peBx9VXG.mjs';
5
+ import { $ as $$AdminLayout } from './AdminLayout_D-_JeUqC.mjs';
6
+
7
+ var __freeze = Object.freeze;
8
+ var __defProp = Object.defineProperty;
9
+ var __template = (cooked, raw) => __freeze(__defProp(cooked, "raw", { value: __freeze(raw || cooked.slice()) }));
10
+ var _a;
11
+ const $$New = createComponent(async ($$result, $$props, $$slots) => {
12
+ const roleOptions = ["super_admin", "admin", "editor", "author", "customer", "guest"];
13
+ return renderTemplate`${renderComponent($$result, "AdminLayout", $$AdminLayout, { "title": "New User" }, { "default": async ($$result2) => renderTemplate(_a || (_a = __template([" ", '<div class="flex-1 overflow-y-auto p-8 pr-12 space-y-8"> <!-- Header --> <div class="surface-tile p-6 flex items-center justify-between"> <div> <h1 class="text-3xl font-black tracking-tighter text-[#0b1222]">Create User</h1> <p class="text-sm text-[#64748b] mt-1 font-medium">Add a new user to the system</p> </div> <a href="/users" class="text-sm font-bold text-[#64748b] hover:text-[#0b1222] transition-colors">\n← Back to users\n</a> </div> <!-- Form --> <div class="surface-tile p-6"> <form id="create-user-form" class="space-y-6 max-w-2xl"> <div> <label for="email" class="block text-sm font-bold text-[#0b1222] mb-2">Email Address</label> <input type="email" id="email" name="email" required 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]" placeholder="user@example.com"> </div> <div> <label for="password" class="block text-sm font-bold text-[#0b1222] mb-2">Password</label> <input type="password" id="password" name="password" required minlength="12" 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]" placeholder="Minimum 12 characters"> <p class="text-xs text-[#64748b] mt-1">Must contain uppercase, lowercase, numbers, and special characters</p> </div> <div> <label for="role" class="block text-sm font-bold text-[#0b1222] mb-2">Role</label> <select id="role" name="role" 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]"> ', ` </select> </div> <div> <label for="tenantId" class="block text-sm font-bold text-[#0b1222] mb-2">Tenant ID (optional)</label> <input type="text" id="tenantId" name="tenantId" 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]" placeholder="Leave empty for global user"> </div> <div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-100"> <a href="/users" class="px-6 py-3 border border-gray-200 rounded-xl text-sm font-bold text-[#64748b] hover:bg-gray-50 transition-colors">
14
+ Cancel
15
+ </a> <button type="submit" class="px-6 py-3 bg-[#0b1222] text-white rounded-xl text-sm font-bold hover:bg-[#1a2332] transition-colors">
16
+ Create User
17
+ </button> </div> </form> <div id="form-message" class="hidden mt-4 p-4 rounded-xl text-sm font-bold"></div> </div> </div> <script>
18
+ document.addEventListener('DOMContentLoaded', () => {
19
+ const form = document.getElementById('create-user-form');
20
+ const message = document.getElementById('form-message');
21
+
22
+ form?.addEventListener('submit', async (e) => {
23
+ e.preventDefault();
24
+ const formData = new FormData(form);
25
+ const body = Object.fromEntries(formData.entries());
26
+
27
+ const res = await fetch('/api/users', {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify(body),
31
+ });
32
+
33
+ const data = await res.json();
34
+
35
+ if (res.ok) {
36
+ message.textContent = 'User created successfully!';
37
+ message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-green-50 text-green-600';
38
+ setTimeout(() => { window.location.href = \`/users/\${data.data.id}\`; }, 1000);
39
+ } else {
40
+ message.textContent = data.error || 'Failed to create user';
41
+ message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-red-50 text-red-600';
42
+ }
43
+ });
44
+ });
45
+ <\/script> `], [" ", '<div class="flex-1 overflow-y-auto p-8 pr-12 space-y-8"> <!-- Header --> <div class="surface-tile p-6 flex items-center justify-between"> <div> <h1 class="text-3xl font-black tracking-tighter text-[#0b1222]">Create User</h1> <p class="text-sm text-[#64748b] mt-1 font-medium">Add a new user to the system</p> </div> <a href="/users" class="text-sm font-bold text-[#64748b] hover:text-[#0b1222] transition-colors">\n← Back to users\n</a> </div> <!-- Form --> <div class="surface-tile p-6"> <form id="create-user-form" class="space-y-6 max-w-2xl"> <div> <label for="email" class="block text-sm font-bold text-[#0b1222] mb-2">Email Address</label> <input type="email" id="email" name="email" required 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]" placeholder="user@example.com"> </div> <div> <label for="password" class="block text-sm font-bold text-[#0b1222] mb-2">Password</label> <input type="password" id="password" name="password" required minlength="12" 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]" placeholder="Minimum 12 characters"> <p class="text-xs text-[#64748b] mt-1">Must contain uppercase, lowercase, numbers, and special characters</p> </div> <div> <label for="role" class="block text-sm font-bold text-[#0b1222] mb-2">Role</label> <select id="role" name="role" 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]"> ', ` </select> </div> <div> <label for="tenantId" class="block text-sm font-bold text-[#0b1222] mb-2">Tenant ID (optional)</label> <input type="text" id="tenantId" name="tenantId" 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]" placeholder="Leave empty for global user"> </div> <div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-100"> <a href="/users" class="px-6 py-3 border border-gray-200 rounded-xl text-sm font-bold text-[#64748b] hover:bg-gray-50 transition-colors">
46
+ Cancel
47
+ </a> <button type="submit" class="px-6 py-3 bg-[#0b1222] text-white rounded-xl text-sm font-bold hover:bg-[#1a2332] transition-colors">
48
+ Create User
49
+ </button> </div> </form> <div id="form-message" class="hidden mt-4 p-4 rounded-xl text-sm font-bold"></div> </div> </div> <script>
50
+ document.addEventListener('DOMContentLoaded', () => {
51
+ const form = document.getElementById('create-user-form');
52
+ const message = document.getElementById('form-message');
53
+
54
+ form?.addEventListener('submit', async (e) => {
55
+ e.preventDefault();
56
+ const formData = new FormData(form);
57
+ const body = Object.fromEntries(formData.entries());
58
+
59
+ const res = await fetch('/api/users', {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify(body),
63
+ });
64
+
65
+ const data = await res.json();
66
+
67
+ if (res.ok) {
68
+ message.textContent = 'User created successfully!';
69
+ message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-green-50 text-green-600';
70
+ setTimeout(() => { window.location.href = \\\`/users/\\\${data.data.id}\\\`; }, 1000);
71
+ } else {
72
+ message.textContent = data.error || 'Failed to create user';
73
+ message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-red-50 text-red-600';
74
+ }
75
+ });
76
+ });
77
+ <\/script> `])), maybeRenderHead(), roleOptions.map((role) => renderTemplate`<option${addAttribute(role, "value")}>${role}</option>`)) })}`;
78
+ }, "/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/pages/users/new.astro", void 0);
79
+
80
+ const $$file = "/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/pages/users/new.astro";
81
+ const $$url = "/users/new";
82
+
83
+ const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
84
+ __proto__: null,
85
+ default: $$New,
86
+ file: $$file,
87
+ url: $$url
88
+ }, Symbol.toStringTag, { value: 'Module' }));
89
+
90
+ const page = () => _page;
91
+
92
+ export { page };