@kyro-cms/admin 0.1.5 → 0.1.7

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 (164) hide show
  1. package/README.md +149 -51
  2. package/package.json +52 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +136 -27
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +50 -0
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +116 -28
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +286 -0
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +50 -20
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +82 -0
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +102 -0
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
  164. package/src/pages/index.astro +0 -225
@@ -0,0 +1,162 @@
1
+ ---
2
+ import AdminLayout from '../../layouts/AdminLayout.astro';
3
+ import { globals } from '@/lib/config';
4
+ import { AutoForm } from '@/components/AutoForm';
5
+
6
+ const { slug } = Astro.params;
7
+
8
+ // Validate global exists
9
+ if (!slug || !globals[slug]) {
10
+ return Astro.redirect('/');
11
+ }
12
+
13
+ const config = globals[slug];
14
+
15
+ // Fetch current settings data from API
16
+ let data: any = {};
17
+ try {
18
+ const response = await fetch(`${Astro.url.origin}/api/globals/${slug}`, {
19
+ headers: Astro.request.headers,
20
+ credentials: 'include'
21
+ });
22
+ if (response.ok) {
23
+ const result = await response.json();
24
+ data = result.data || {};
25
+ }
26
+ } catch (error) {
27
+ console.error('Failed to fetch settings:', error);
28
+ }
29
+
30
+ const activeSlug = slug;
31
+ const allGlobals = Object.values(globals);
32
+
33
+ const title = config.label || slug;
34
+ const description = config.admin?.description || `Manage your ${title.toLowerCase()} configurations.`;
35
+ ---
36
+
37
+ <AdminLayout title="Settings">
38
+ <div class="flex-1 overflow-y-auto p-8 space-y-6">
39
+ <!-- Header -->
40
+ <div class="surface-tile p-8 pb-0">
41
+ <div class="flex items-center justify-between mb-8">
42
+ <div>
43
+ <h1 class="text-3xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
44
+ Settings
45
+ </h1>
46
+ <p class="text-sm text-[var(--kyro-text-secondary)] mt-1 font-medium italic">
47
+ Global system configurations
48
+ </p>
49
+ </div>
50
+ <button
51
+ type="submit"
52
+ form="settings-form"
53
+ id="btn-save"
54
+ class="px-8 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold transition-all hover:opacity-90 active:scale-95 shadow-lg shadow-black/10"
55
+ >
56
+ Save Changes
57
+ </button>
58
+ </div>
59
+
60
+ <!-- Tabs Bar -->
61
+ <div class="flex items-center gap-8 border-b border-[var(--kyro-border)] overflow-x-auto no-scrollbar">
62
+ {
63
+ allGlobals.map((g) => (
64
+ <a
65
+ href={`/settings/${g.slug}`}
66
+ class={`pb-4 text-sm font-bold transition-all border-b-2 whitespace-nowrap ${
67
+ activeSlug === g.slug
68
+ ? "text-[var(--kyro-text-primary)] border-[var(--kyro-text-primary)]"
69
+ : "text-[var(--kyro-text-secondary)] border-transparent hover:text-[var(--kyro-text-primary)]"
70
+ }`}
71
+ >
72
+ {g.label || g.slug}
73
+ </a>
74
+ ))
75
+ }
76
+ </div>
77
+ </div>
78
+
79
+ <!-- Form Section -->
80
+ <div class="surface-tile p-10 pt-4">
81
+ <div class="mb-8">
82
+ <h2 class="text-xl font-black text-[var(--kyro-text-primary)] tracking-tight">{config.label || slug}</h2>
83
+ <p class="text-sm text-[var(--kyro-text-secondary)] mt-1">{description}</p>
84
+ </div>
85
+ <!-- Toast container -->
86
+ <div id="toast-container" class="hidden fixed bottom-6 right-6 z-50">
87
+ <div class="flex items-center gap-3 px-5 py-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl shadow-2xl">
88
+ <span id="toast-message"></span>
89
+ <button onclick="document.getElementById('toast-container').classList.add('hidden')" class="opacity-50 hover:opacity-100 ml-2">
90
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
91
+ </button>
92
+ </div>
93
+ </div>
94
+
95
+ <form id="settings-form">
96
+ <AutoForm
97
+ client:only="react"
98
+ config={config}
99
+ globalSlug={slug}
100
+ data={data}
101
+ layout="single"
102
+ />
103
+ <input type="hidden" id="form-data" name="form-data" value="{}" />
104
+ </form>
105
+ </div>
106
+ </div>
107
+
108
+ <script define:vars={{ slug }}>
109
+ function showToast(message, isError = false) {
110
+ const container = document.getElementById('toast-container');
111
+ const msg = document.getElementById('toast-message');
112
+ if (container && msg) {
113
+ msg.textContent = message;
114
+ container.classList.remove('hidden');
115
+ if (!isError) {
116
+ setTimeout(() => container.classList.add('hidden'), 3000);
117
+ }
118
+ }
119
+ }
120
+
121
+ document.getElementById('settings-form')?.addEventListener('submit', async (e) => {
122
+ e.preventDefault();
123
+
124
+ const btn = document.getElementById('btn-save');
125
+ const originalText = btn?.textContent || '';
126
+ if (btn) {
127
+ btn.textContent = 'Saving...';
128
+ btn.setAttribute('disabled', 'true');
129
+ }
130
+
131
+ // Read data from the hidden input that AutoForm writes to internally
132
+ const hiddenInput = document.getElementById('form-data');
133
+ let payload = {};
134
+ if (hiddenInput && hiddenInput.value) {
135
+ try { payload = JSON.parse(hiddenInput.value); } catch(e) {}
136
+ }
137
+
138
+ try {
139
+ const response = await fetch(`/api/globals/${slug}`, {
140
+ method: 'PATCH',
141
+ credentials: 'include',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify(payload),
144
+ });
145
+
146
+ if (response.ok) {
147
+ showToast('Settings saved successfully');
148
+ } else {
149
+ const error = await response.json();
150
+ showToast(error.error || 'Failed to save settings', true);
151
+ }
152
+ } catch (error) {
153
+ showToast('An error occurred while saving', true);
154
+ } finally {
155
+ if (btn) {
156
+ btn.textContent = originalText;
157
+ btn.removeAttribute('disabled');
158
+ }
159
+ }
160
+ });
161
+ </script>
162
+ </AdminLayout>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import { globals } from '@/lib/config';
3
+
4
+ const firstGlobal = Object.values(globals)[0];
5
+ if (firstGlobal) {
6
+ return Astro.redirect(`/settings/${firstGlobal.slug}`);
7
+ }
8
+ return Astro.redirect('/');
9
+ ---
@@ -7,7 +7,10 @@ let error: string | null = null;
7
7
 
8
8
  if (id) {
9
9
  try {
10
- const response = await fetch(`${Astro.url.origin}/api/users/${id}`);
10
+ const response = await fetch(`${Astro.url.origin}/api/users/${id}`, {
11
+ headers: Astro.request.headers,
12
+ credentials: 'include'
13
+ });
11
14
  if (response.ok) {
12
15
  const data = await response.json();
13
16
  user = data.data;
@@ -27,8 +30,8 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
27
30
  {error ? (
28
31
  <div class="surface-tile p-8">
29
32
  <div class="text-center">
30
- <p class="text-lg font-bold text-red-600">{error}</p>
31
- <a href="/users" class="mt-4 inline-block text-[#0b1222] font-bold underline">← Back to users</a>
33
+ <p class="text-lg font-bold text-red-500">{error}</p>
34
+ <a href="/users" class="mt-4 inline-block text-[var(--kyro-text-primary)] font-bold underline">← Back to users</a>
32
35
  </div>
33
36
  </div>
34
37
  ) : user ? (
@@ -36,16 +39,16 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
36
39
  <!-- Header -->
37
40
  <div class="surface-tile p-6 flex items-center justify-between">
38
41
  <div class="flex items-center gap-4">
39
- <div class="w-14 h-14 rounded-full bg-[#0b1222] text-white flex items-center justify-center font-bold text-xl">
42
+ <div class="w-14 h-14 rounded-full bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] flex items-center justify-center font-bold text-xl">
40
43
  {user.email.charAt(0).toUpperCase()}
41
44
  </div>
42
45
  <div>
43
- <h1 class="text-2xl font-black tracking-tighter text-[#0b1222]">{user.email}</h1>
44
- <p class="text-sm text-[#64748b] font-medium">User ID: {user.id}</p>
46
+ <h1 class="text-2xl font-black tracking-tighter text-[var(--kyro-text-primary)]">{user.email}</h1>
47
+ <p class="text-sm text-[var(--kyro-text-secondary)] font-medium">User ID: {user.id}</p>
45
48
  </div>
46
49
  </div>
47
50
  <div class="flex gap-2">
48
- <button id="btn-toggle-lock" class="px-4 py-2 border border-gray-200 rounded-xl text-sm font-bold text-[#64748b] hover:bg-gray-50 transition-colors">
51
+ <button id="btn-toggle-lock" class="px-4 py-2 border border-[var(--kyro-border)] rounded-xl text-sm font-bold text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
49
52
  {user.locked ? 'Unlock User' : 'Lock User'}
50
53
  </button>
51
54
  <button id="btn-delete-user" class="px-4 py-2 border border-red-200 rounded-xl text-sm font-bold text-red-600 hover:bg-red-50 transition-colors">
@@ -56,45 +59,45 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
56
59
 
57
60
  <!-- User Details -->
58
61
  <div class="surface-tile p-6">
59
- <h2 class="text-lg font-black text-[#0b1222] tracking-tighter mb-6">Details</h2>
62
+ <h2 class="text-lg font-black text-[var(--kyro-text-primary)] tracking-tighter mb-6">Details</h2>
60
63
  <div class="grid grid-cols-2 gap-6">
61
64
  <div>
62
- <label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Email</label>
63
- <p class="mt-1 font-bold text-[#0b1222]" id="field-email">{user.email}</p>
65
+ <label class="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-wider">Email</label>
66
+ <p class="mt-1 font-bold text-[var(--kyro-text-primary)]" id="field-email">{user.email}</p>
64
67
  </div>
65
68
  <div>
66
- <label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Role</label>
67
- <select id="field-role" class="mt-1 w-full px-3 py-2 border border-gray-200 rounded-lg text-sm font-bold text-[#0b1222]">
69
+ <label class="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-wider">Role</label>
70
+ <select id="field-role" class="mt-1 w-full px-3 py-2 border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] rounded-lg text-sm font-bold text-[var(--kyro-text-primary)]">
68
71
  {roleOptions.map((role) => (
69
72
  <option value={role} selected={user.role === role}>{role}</option>
70
73
  ))}
71
74
  </select>
72
75
  </div>
73
76
  <div>
74
- <label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Email Verified</label>
77
+ <label class="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-wider">Email Verified</label>
75
78
  <p class="mt-1">
76
- <span class={`inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold ${user.emailVerified ? 'bg-green-50 text-green-600' : 'bg-yellow-50 text-yellow-600'}`}>
79
+ <span class={`inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold ${user.emailVerified ? 'bg-green-500/10 text-green-500' : 'bg-yellow-500/10 text-yellow-500'}`}>
77
80
  {user.emailVerified ? 'Verified' : 'Not verified'}
78
81
  </span>
79
82
  </p>
80
83
  </div>
81
84
  <div>
82
- <label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Status</label>
85
+ <label class="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-wider">Status</label>
83
86
  <p class="mt-1">
84
- <span class={`inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold ${user.locked ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
87
+ <span class={`inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold ${user.locked ? 'bg-red-500/10 text-red-500' : 'bg-green-500/10 text-green-500'}`}>
85
88
  {user.locked ? 'Locked' : 'Active'}
86
89
  </span>
87
90
  </p>
88
91
  </div>
89
92
  <div>
90
- <label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Last Login</label>
91
- <p class="mt-1 text-sm text-[#64748b]" id="field-last-login">
93
+ <label class="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-wider">Last Login</label>
94
+ <p class="mt-1 text-sm text-[var(--kyro-text-secondary)]" id="field-last-login">
92
95
  {user.lastLogin ? new Date(user.lastLogin).toLocaleString() : 'Never'}
93
96
  </p>
94
97
  </div>
95
98
  <div>
96
- <label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Failed Attempts</label>
97
- <p class="mt-1 text-sm font-bold text-[#0b1222]" id="field-failed-attempts">
99
+ <label class="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-wider">Failed Attempts</label>
100
+ <p class="mt-1 text-sm font-bold text-[var(--kyro-text-primary)]" id="field-failed-attempts">
98
101
  {user.failedLoginAttempts || 0}
99
102
  </p>
100
103
  </div>
@@ -139,6 +142,7 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
139
142
  const role = roleSelect.value;
140
143
  const res = await fetch(`/api/users/${userId}`, {
141
144
  method: 'PATCH',
145
+ credentials: 'include',
142
146
  headers: { 'Content-Type': 'application/json' },
143
147
  body: JSON.stringify({ role }),
144
148
  });
@@ -154,6 +158,7 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
154
158
  const isLocked = lockBtn.textContent?.includes('Unlock');
155
159
  const res = await fetch(`/api/users/${userId}`, {
156
160
  method: 'PATCH',
161
+ credentials: 'include',
157
162
  headers: { 'Content-Type': 'application/json' },
158
163
  body: JSON.stringify({ locked: isLocked }),
159
164
  });
@@ -164,7 +169,10 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
164
169
  if (deleteBtn) {
165
170
  deleteBtn.addEventListener('click', async () => {
166
171
  if (confirm('Delete this user? This cannot be undone.')) {
167
- const res = await fetch(`/api/users/${userId}`, { method: 'DELETE' });
172
+ const res = await fetch(`/api/users/${userId}`, {
173
+ method: 'DELETE',
174
+ credentials: 'include'
175
+ });
168
176
  if (res.ok) window.location.href = '/users';
169
177
  }
170
178
  });
@@ -5,7 +5,10 @@ let users: any[] = [];
5
5
  let totalUsers = 0;
6
6
 
7
7
  try {
8
- const response = await fetch(`${Astro.url.origin}/api/users?page=1&limit=100`);
8
+ const response = await fetch(`${Astro.url.origin}/api/users?page=1&limit=100`, {
9
+ headers: Astro.request.headers,
10
+ credentials: 'include'
11
+ });
9
12
  if (response.ok) {
10
13
  const data = await response.json();
11
14
  users = data.docs || [];
@@ -15,6 +18,8 @@ try {
15
18
  console.error('Failed to fetch users:', error);
16
19
  }
17
20
 
21
+ users = users || [];
22
+
18
23
  const roleColors: Record<string, string> = {
19
24
  super_admin: 'bg-red-50 text-red-600',
20
25
  admin: 'bg-purple-50 text-purple-600',
@@ -30,13 +35,13 @@ const roleColors: Record<string, string> = {
30
35
  <!-- Header -->
31
36
  <div class="surface-tile p-6 flex items-center justify-between">
32
37
  <div>
33
- <h1 class="text-3xl font-black tracking-tighter text-[#0b1222]">Users</h1>
34
- <p class="text-sm text-[#64748b] mt-1 font-medium">
38
+ <h1 class="text-3xl font-black tracking-tighter text-[var(--kyro-text-primary)]">Users</h1>
39
+ <p class="text-sm text-[var(--kyro-text-secondary)] mt-1 font-medium">
35
40
  Manage user accounts and permissions
36
- <span class="ml-2 text-[#0b1222] font-bold">· {totalUsers} users</span>
41
+ <span class="ml-2 text-[var(--kyro-text-primary)] font-bold">· {totalUsers} users</span>
37
42
  </p>
38
43
  </div>
39
- <a href="/users/new" class="flex items-center gap-2 px-6 py-3 bg-[#0b1222] text-white rounded-xl font-bold transition-all hover:bg-[#1a2332] active:scale-95">
44
+ <a href="/users/new" class="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold transition-all hover:opacity-90 active:scale-95 shadow-lg">
40
45
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
41
46
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"></path>
42
47
  </svg>
@@ -49,14 +54,14 @@ const roleColors: Record<string, string> = {
49
54
  {users.length === 0 ? (
50
55
  <div class="px-8 py-16 text-center">
51
56
  <div class="flex flex-col items-center gap-4">
52
- <div class="w-16 h-16 rounded-2xl bg-gray-50 flex items-center justify-center">
53
- <svg class="w-8 h-8 text-[#9ca3af]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
57
+ <div class="w-16 h-16 rounded-2xl bg-[var(--kyro-surface-accent)] flex items-center justify-center">
58
+ <svg class="w-8 h-8 text-[var(--kyro-text-muted)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54
59
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
55
60
  </svg>
56
61
  </div>
57
- <p class="font-bold text-[#0b1222] text-base">No users yet</p>
58
- <p class="text-sm text-[#64748b]">Create your first user to get started.</p>
59
- <a href="/users/new" class="mt-2 inline-flex items-center gap-2 px-5 py-2.5 bg-[#0b1222] text-white rounded-lg font-bold text-sm">
62
+ <p class="font-bold text-[var(--kyro-text-primary)] text-base">No users yet</p>
63
+ <p class="text-sm text-[var(--kyro-text-secondary)]">Create your first user to get started.</p>
64
+ <a href="/users/new" class="mt-2 inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg font-bold text-sm shadow-md">
60
65
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
61
66
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"></path>
62
67
  </svg>
@@ -67,7 +72,7 @@ const roleColors: Record<string, string> = {
67
72
  ) : (
68
73
  <table class="w-full text-left">
69
74
  <thead>
70
- <tr class="text-[#64748b] font-bold text-[10px] uppercase tracking-[0.3em] border-b border-gray-100">
75
+ <tr class="text-[var(--kyro-text-secondary)] font-bold text-[10px] uppercase tracking-[0.3em] border-b border-[var(--kyro-border)]">
71
76
  <th class="px-8 py-6">Email</th>
72
77
  <th class="px-6 py-6">Role</th>
73
78
  <th class="px-6 py-6">Status</th>
@@ -76,17 +81,17 @@ const roleColors: Record<string, string> = {
76
81
  <th class="px-6 py-6 text-right">Actions</th>
77
82
  </tr>
78
83
  </thead>
79
- <tbody class="divide-y divide-gray-50">
84
+ <tbody class="divide-y divide-[var(--kyro-border)]">
80
85
  {users.map((user) => (
81
86
  <tr class="group hover:bg-gray-50/50 transition-colors cursor-pointer" onclick={`window.location='/users/${user.id}'`}>
82
87
  <td class="px-8 py-5">
83
88
  <div class="flex items-center gap-4">
84
- <div class="w-10 h-10 rounded-full bg-[#0b1222] text-white flex items-center justify-center font-bold text-sm">
85
- {user.email.charAt(0).toUpperCase()}
89
+ <div class="w-10 h-10 rounded-full bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] flex items-center justify-center font-bold text-sm">
90
+ {user.email ? user.email.charAt(0).toUpperCase() : '?'}
86
91
  </div>
87
92
  <div>
88
- <div class="font-bold text-[#0b1222]">{user.email}</div>
89
- {user.tenantId && <div class="text-xs text-[#64748b]">Tenant: {user.tenantId}</div>}
93
+ <div class="font-bold text-[var(--kyro-text-primary)]">{user.email}</div>
94
+ {user.tenantId && <div class="text-xs text-[var(--kyro-text-secondary)]">Tenant: {user.tenantId}</div>}
90
95
  </div>
91
96
  </div>
92
97
  </td>
@@ -125,7 +130,7 @@ const roleColors: Record<string, string> = {
125
130
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
126
131
  </svg>
127
132
  </a>
128
- <button onclick={`event.stopPropagation(); if(confirm('Delete this user?')) { fetch('/api/users/${user.id}', { method: 'DELETE' }).then(() => location.reload()); }`} class="inline-flex items-center justify-center w-8 h-8 rounded-md text-[#64748b] hover:bg-red-50 hover:text-red-600 transition-colors">
133
+ <button onclick={`event.stopPropagation(); if(confirm('Delete this user?')) { fetch('/api/users/${user.id}', { method: 'DELETE', credentials: 'include' }).then(() => location.reload()); }`} class="inline-flex items-center justify-center w-8 h-8 rounded-md text-[#64748b] hover:bg-red-50 hover:text-red-600 transition-colors">
129
134
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
135
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
131
136
  </svg>
@@ -9,10 +9,10 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
9
9
  <!-- Header -->
10
10
  <div class="surface-tile p-6 flex items-center justify-between">
11
11
  <div>
12
- <h1 class="text-3xl font-black tracking-tighter text-[#0b1222]">Create User</h1>
13
- <p class="text-sm text-[#64748b] mt-1 font-medium">Add a new user to the system</p>
12
+ <h1 class="text-3xl font-black tracking-tighter text-[var(--kyro-text-primary)]">Create User</h1>
13
+ <p class="text-sm text-[var(--kyro-text-secondary)] mt-1 font-medium">Add a new user to the system</p>
14
14
  </div>
15
- <a href="/users" class="text-sm font-bold text-[#64748b] hover:text-[#0b1222] transition-colors">
15
+ <a href="/users" class="text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors">
16
16
  ← Back to users
17
17
  </a>
18
18
  </div>
@@ -21,19 +21,19 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
21
21
  <div class="surface-tile p-6">
22
22
  <form id="create-user-form" class="space-y-6 max-w-2xl">
23
23
  <div>
24
- <label for="email" class="block text-sm font-bold text-[#0b1222] mb-2">Email Address</label>
25
- <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" />
24
+ <label for="email" class="block text-sm font-bold text-[var(--kyro-text-primary)] mb-2">Email Address</label>
25
+ <input type="email" id="email" name="email" required class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]" placeholder="user@example.com" />
26
26
  </div>
27
27
 
28
28
  <div>
29
- <label for="password" class="block text-sm font-bold text-[#0b1222] mb-2">Password</label>
30
- <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" />
31
- <p class="text-xs text-[#64748b] mt-1">Must contain uppercase, lowercase, numbers, and special characters</p>
29
+ <label for="password" class="block text-sm font-bold text-[var(--kyro-text-primary)] mb-2">Password</label>
30
+ <input type="password" id="password" name="password" required minlength="12" class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]" placeholder="Minimum 12 characters" />
31
+ <p class="text-xs text-[var(--kyro-text-secondary)] mt-1">Must contain uppercase, lowercase, numbers, and special characters</p>
32
32
  </div>
33
33
 
34
34
  <div>
35
- <label for="role" class="block text-sm font-bold text-[#0b1222] mb-2">Role</label>
36
- <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]">
35
+ <label for="role" class="block text-sm font-bold text-[var(--kyro-text-primary)] mb-2">Role</label>
36
+ <select id="role" name="role" class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]">
37
37
  {roleOptions.map((role) => (
38
38
  <option value={role}>{role}</option>
39
39
  ))}
@@ -41,15 +41,15 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
41
41
  </div>
42
42
 
43
43
  <div>
44
- <label for="tenantId" class="block text-sm font-bold text-[#0b1222] mb-2">Tenant ID (optional)</label>
45
- <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" />
44
+ <label for="tenantId" class="block text-sm font-bold text-[var(--kyro-text-primary)] mb-2">Tenant ID (optional)</label>
45
+ <input type="text" id="tenantId" name="tenantId" class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]" placeholder="Leave empty for global user" />
46
46
  </div>
47
47
 
48
- <div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-100">
49
- <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">
48
+ <div class="flex items-center justify-end gap-3 pt-4 border-t border-[var(--kyro-border)]">
49
+ <a href="/users" class="px-6 py-3 border border-[var(--kyro-border)] rounded-xl text-sm font-bold text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
50
50
  Cancel
51
51
  </a>
52
- <button type="submit" class="px-6 py-3 bg-[#0b1222] text-white rounded-xl text-sm font-bold hover:bg-[#1a2332] transition-colors">
52
+ <button type="submit" class="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-sm font-bold hover:bg-opacity-90 transition-colors shadow-lg shadow-black/10">
53
53
  Create User
54
54
  </button>
55
55
  </div>
@@ -71,6 +71,7 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
71
71
 
72
72
  const res = await fetch('/api/users', {
73
73
  method: 'POST',
74
+ credentials: 'include',
74
75
  headers: { 'Content-Type': 'application/json' },
75
76
  body: JSON.stringify(body),
76
77
  });
@@ -79,11 +80,11 @@ const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'gu
79
80
 
80
81
  if (res.ok) {
81
82
  message.textContent = 'User created successfully!';
82
- message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-green-50 text-green-600';
83
+ message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-green-500/10 text-green-500';
83
84
  setTimeout(() => { window.location.href = `/users/${data.data.id}`; }, 1000);
84
85
  } else {
85
86
  message.textContent = data.error || 'Failed to create user';
86
- message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-red-50 text-red-600';
87
+ message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-red-500/10 text-red-500';
87
88
  }
88
89
  });
89
90
  });