@kyro-cms/admin 0.12.20 → 0.12.22

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 (56) hide show
  1. package/dist/index.cjs +24 -24
  2. package/dist/index.d.cts +2 -1
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js +19 -19
  5. package/package.json +6 -6
  6. package/src/components/ApiKeysManager.tsx +177 -227
  7. package/src/components/AuditLogsPage.tsx +7 -7
  8. package/src/components/AuthorDashboard.tsx +235 -0
  9. package/src/components/AutoForm.tsx +14 -4
  10. package/src/components/CustomerDashboard.tsx +213 -0
  11. package/src/components/Dashboard.tsx +1 -1
  12. package/src/components/DashboardMetrics.tsx +74 -9
  13. package/src/components/DashboardQuickSections.tsx +237 -0
  14. package/src/components/ListView.tsx +3 -3
  15. package/src/components/MediaGallery.tsx +2 -2
  16. package/src/components/PluginsManager.tsx +1 -1
  17. package/src/components/SessionsManager.tsx +15 -15
  18. package/src/components/Sidebar.astro +564 -116
  19. package/src/components/UserManagement.tsx +43 -9
  20. package/src/components/UserMenu.tsx +123 -61
  21. package/src/components/autoform/AutoFormEditView.tsx +4 -4
  22. package/src/components/autoform/AutoFormHeader.tsx +2 -2
  23. package/src/components/ui/CommandPalette.tsx +1 -1
  24. package/src/components/ui/Dropdown.tsx +71 -16
  25. package/src/components/ui/PageHeader.tsx +1 -1
  26. package/src/components/ui/Pagination.tsx +1 -1
  27. package/src/components/ui/Toast.tsx +3 -2
  28. package/src/components/ui/Toaster.tsx +1 -1
  29. package/src/components/users/UserDetail.tsx +74 -29
  30. package/src/components/users/UserForm.tsx +1 -1
  31. package/src/components/users/UsersList.tsx +2 -2
  32. package/src/integration.ts +4 -0
  33. package/src/layouts/AdminLayout.astro +26 -34
  34. package/src/layouts/AuthLayout.astro +18 -9
  35. package/src/lib/auth-server.ts +99 -0
  36. package/src/pages/403.astro +54 -35
  37. package/src/pages/[collection]/[id].astro +8 -0
  38. package/src/pages/[collection]/index.astro +7 -0
  39. package/src/pages/audit/index.astro +4 -1
  40. package/src/pages/auth/check-email.astro +146 -0
  41. package/src/pages/auth/forgot-password.astro +95 -0
  42. package/src/pages/auth/login.astro +29 -12
  43. package/src/pages/auth/register.astro +38 -12
  44. package/src/pages/auth/reset-password.astro +125 -0
  45. package/src/pages/auth/verify-email.astro +87 -0
  46. package/src/pages/index.astro +220 -121
  47. package/src/pages/keys.astro +3 -1
  48. package/src/pages/media.astro +3 -1
  49. package/src/pages/preview/[collection]/[id].astro +23 -73
  50. package/src/pages/roles/index.astro +134 -101
  51. package/src/pages/settings/[slug].astro +43 -3
  52. package/src/pages/users/[id].astro +5 -1
  53. package/src/pages/users/index.astro +4 -0
  54. package/src/pages/users/new.astro +4 -0
  55. package/src/pages/webhooks.astro +3 -1
  56. package/src/routes.ts +16 -0
@@ -5,18 +5,64 @@ import { collections } from "../lib/config";
5
5
  const authCollections = ["users", "audit_logs"];
6
6
  import { adminPath, apiPath } from "../lib/paths";
7
7
  import { DashboardMetrics, RevenueChart } from "../components/DashboardMetrics";
8
+ import { CustomerDashboard } from "../components/CustomerDashboard";
9
+ import { AuthorDashboard } from "../components/AuthorDashboard";
10
+ import { DashboardQuickSections } from "../components/DashboardQuickSections";
8
11
 
9
- const authItems = authCollections.map((slug) => ({
10
- slug,
11
- label: collections[slug]?.label || slug,
12
- icon: slug === "users" ? "users" : "file-text",
13
- }));
12
+ import { getAuthServer } from "../lib/auth-server";
13
+
14
+ const { user: serverUser, permissions: serverPermissions } = await getAuthServer(Astro.request);
15
+ const serverRole = serverUser?.role || "";
16
+ const isServerSuperAdmin = serverRole === "super_admin";
17
+ const isServerAdmin = serverRole === "admin" || isServerSuperAdmin;
18
+ const isServerCustomer = serverRole === "customer";
19
+ const isServerAuthor = serverRole === "author";
20
+
21
+ // Serialize collections for client-side role-based dashboards
22
+ const serializedCollections = JSON.parse(JSON.stringify(
23
+ Object.fromEntries(
24
+ Object.entries(collections).map(([slug, config]) => [slug, { label: (config as any).label || slug, slug }])
25
+ )
26
+ ));
27
+
28
+ const permittedQuickLinks = Object.entries(collections)
29
+ .filter(([slug]) => !["users", "audit_logs", "media"].includes(slug))
30
+ .filter(([slug]) => {
31
+ if (!serverUser) return true;
32
+ return isServerAdmin || serverPermissions?.collections?.[slug]?.create === true;
33
+ })
34
+ .slice(0, 6);
35
+
36
+ const canSeeAudit = !serverUser || isServerAdmin || serverPermissions?.collections?.audit_logs?.read === true;
37
+ const canSeeLockedUsers = !serverUser || isServerAdmin || serverPermissions?.collections?.users?.read === true;
38
+ const canSeeRoles = !serverUser || isServerSuperAdmin;
39
+ const canSeeHealth = !serverUser || isServerAdmin;
40
+ const hasAnySecurityAction = canSeeAudit || canSeeLockedUsers || canSeeRoles || canSeeHealth;
14
41
  ---
15
42
 
16
43
  <AdminLayout title={i18next.t("pages." + "Dashboard".replace(/ /g, ""), { defaultValue: "Dashboard" })}>
17
44
  <div class="flex-1 overflow-y-auto space-y-8">
45
+ {/* ── Role-Based Dashboard Fork ── */}
46
+ {isServerCustomer ? (
47
+ <CustomerDashboard
48
+ client:load
49
+ collections={serializedCollections}
50
+ userName={serverUser?.name || serverUser?.email || ""}
51
+ userAvatar={serverUser?.avatar || ""}
52
+ userId={serverUser?.id || ""}
53
+ />
54
+ ) : isServerAuthor ? (
55
+ <AuthorDashboard
56
+ client:load
57
+ collections={serializedCollections}
58
+ userName={serverUser?.name || serverUser?.email || ""}
59
+ userAvatar={serverUser?.avatar || ""}
60
+ userId={serverUser?.id || ""}
61
+ />
62
+ ) : (
63
+ <>
18
64
  <!-- Header -->
19
- <div class="surface-tile p-6 flex items-center justify-between gap-8">
65
+ <div class="surface-tile p-6 flex items-center justify-between rounded-lg gap-8 rounded-lg">
20
66
  <div class="relative flex-1 max-w-2xl">
21
67
  <div class="absolute inset-y-0 left-6 flex items-center pointer-events-none text-[var(--kyro-text-muted)]">
22
68
  <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -51,128 +97,179 @@ const authItems = authCollections.map((slug) => ({
51
97
  </div>
52
98
 
53
99
  <!-- CMS Metrics -->
54
- <DashboardMetrics client:load isEcommerce={!!collections["orders"] || !!collections["products"]} />
55
- {(!!collections["orders"] || !!collections["products"]) && <RevenueChart client:load />}
56
-
57
- <!-- Quick Links Section -->
58
- <div class="surface-tile overflow-hidden mt-8">
59
- <div class="flex items-center justify-between p-6 border-b border-[var(--kyro-border)]">
60
- <div class="flex-1">
61
- <h2 class="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)] flex items-center gap-2">
62
- Quick Links
63
- </h2>
64
- <p class="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">Create new collection documents</p>
100
+ <DashboardMetrics client:only="react" isEcommerce={!!collections["orders"] || !!collections["products"]} />
101
+ {(!!collections["orders"] || !!collections["products"]) && <RevenueChart client:only="react" />}
102
+
103
+
104
+ <!-- Role-filtered Quick Actions & Explore Content -->
105
+ <DashboardQuickSections
106
+ client:only="react"
107
+ collections={serializedCollections}
108
+ serverUserRole={serverRole}
109
+ serverPermissions={serverPermissions}
110
+ />
111
+
112
+ <!-- Security Quick Actions -->
113
+ {hasAnySecurityAction && (
114
+ <div id="dashboard-security-section" class="surface-tile overflow-hidden rounded-lg rounded-lg mt-6">
115
+ <div class="p-6 border-b border-[var(--kyro-border)]">
116
+ <h2 class="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">Security & Monitoring</h2>
117
+ <p class="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">
118
+ Rate limiting, audit logs, and account lockout settings
119
+ </p>
65
120
  </div>
66
- </div>
121
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 p-6">
122
+ {canSeeAudit && (
123
+ <a
124
+ href={`${adminPath}/audit`}
125
+ data-sec-card="audit"
126
+ class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
127
+ >
128
+ <div class="w-10 h-10 rounded-lg bg-orange-500/10 text-orange-500 flex items-center justify-center mb-3">
129
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
+ <path
131
+ stroke-linecap="round"
132
+ stroke-linejoin="round"
133
+ stroke-width="2"
134
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
135
+ ></path>
136
+ </svg>
137
+ </div>
138
+ <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">Audit Logs</h4>
139
+ <p class="text-xs text-[var(--kyro-text-secondary)]">View last 30 days</p>
140
+ </a>
141
+ )}
67
142
 
68
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4 p-4">
69
- {
70
- Object.entries(collections)
71
- .filter(([slug]) => !["users", "audit_logs", "media"].includes(slug))
72
- .slice(0, 6)
73
- .map(([slug, config]: [string, any]) => (
74
- <a
75
- href={`${adminPath}/${slug}/new`}
76
- class="group p-4 bg-[var(--kyro-surface-accent)] rounded-xl hover:bg-[var(--kyro-surface)] transition-all border border-transparent hover:border-[var(--kyro-border)] shadow-sm hover:shadow-md"
77
- >
78
- <div class="flex items-center gap-3">
79
- <div class="w-8 h-8 rounded-lg flex items-center justify-center bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] group-hover:bg-[var(--kyro-surface-accent)] group-hover:text-[var(--kyro-primary)] transition-all">
80
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
81
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14M5 12h14" />
82
- </svg>
83
- </div>
84
- <span class="text-sm font-medium text-[var(--kyro-text-primary)]">New {config.label || slug}</span>
85
- </div>
86
- </a>
87
- ))
88
- }
89
- </div>
90
- </div>
143
+ {canSeeLockedUsers && (
144
+ <a
145
+ href={`${adminPath}/users?locked=true`}
146
+ data-sec-card="locked-users"
147
+ class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
148
+ >
149
+ <div class="w-10 h-10 rounded-lg bg-red-500/10 text-red-500 flex items-center justify-center mb-3">
150
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
151
+ <path
152
+ stroke-linecap="round"
153
+ stroke-linejoin="round"
154
+ stroke-width="2"
155
+ d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
156
+ ></path>
157
+ </svg>
158
+ </div>
159
+ <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">Locked Accounts</h4>
160
+ <p class="text-xs text-[var(--kyro-text-secondary)]">Manage lockouts</p>
161
+ </a>
162
+ )}
91
163
 
92
- <!-- Security Quick Actions -->
93
- <div class="surface-tile overflow-hidden">
94
- <div class="p-6 border-b border-[var(--kyro-border)]">
95
- <h2 class="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">Security & Monitoring</h2>
96
- <p class="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">
97
- Rate limiting, audit logs, and account lockout settings
98
- </p>
99
- </div>
100
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 p-6">
101
- <a
102
- href={`${adminPath}/audit`}
103
- class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
104
- >
105
- <div class="w-10 h-10 rounded-lg bg-orange-500/10 text-orange-500 flex items-center justify-center mb-3">
106
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
107
- <path
108
- stroke-linecap="round"
109
- stroke-linejoin="round"
110
- stroke-width="2"
111
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
112
- ></path>
113
- </svg>
114
- </div>
115
- <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">Audit Logs</h4>
116
- <p class="text-xs text-[var(--kyro-text-secondary)]">View last 30 days</p>
117
- </a>
118
-
119
- <a
120
- href={`${adminPath}/users?locked=true`}
121
- class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
122
- >
123
- <div class="w-10 h-10 rounded-lg bg-red-500/10 text-red-500 flex items-center justify-center mb-3">
124
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
125
- <path
126
- stroke-linecap="round"
127
- stroke-linejoin="round"
128
- stroke-width="2"
129
- d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
130
- ></path>
131
- </svg>
132
- </div>
133
- <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">Locked Accounts</h4>
134
- <p class="text-xs text-[var(--kyro-text-secondary)]">Manage lockouts</p>
135
- </a>
136
-
137
- <a
138
- href={`${adminPath}/roles`}
139
- class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
140
- >
141
- <div class="w-10 h-10 rounded-lg bg-indigo-500/10 text-indigo-500 flex items-center justify-center mb-3">
142
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
143
- <path
144
- stroke-linecap="round"
145
- stroke-linejoin="round"
146
- stroke-width="2"
147
- d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
148
- ></path>
149
- </svg>
150
- </div>
151
- <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">Permissions</h4>
152
- <p class="text-xs text-[var(--kyro-text-secondary)]">RBAC settings</p>
153
- </a>
154
-
155
- <a
156
- href={`${adminPath}/health`}
157
- class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
158
- >
159
- <div class="w-10 h-10 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center mb-3">
160
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
161
- <path
162
- stroke-linecap="round"
163
- stroke-linejoin="round"
164
- stroke-width="2"
165
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
166
- </svg>
167
- </div>
168
- <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">API Health</h4>
169
- <p class="text-xs text-[var(--kyro-text-secondary)]">System status</p>
170
- </a>
164
+ {canSeeRoles && (
165
+ <a
166
+ href={`${adminPath}/roles`}
167
+ data-sec-card="roles"
168
+ class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
169
+ >
170
+ <div class="w-10 h-10 rounded-lg bg-indigo-500/10 text-indigo-500 flex items-center justify-center mb-3">
171
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
172
+ <path
173
+ stroke-linecap="round"
174
+ stroke-linejoin="round"
175
+ stroke-width="2"
176
+ d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
177
+ ></path>
178
+ </svg>
179
+ </div>
180
+ <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">Permissions</h4>
181
+ <p class="text-xs text-[var(--kyro-text-secondary)]">RBAC settings</p>
182
+ </a>
183
+ )}
184
+
185
+ {canSeeHealth && (
186
+ <a
187
+ href={`${adminPath}/health`}
188
+ data-sec-card="health"
189
+ class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
190
+ >
191
+ <div class="w-10 h-10 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center mb-3">
192
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
193
+ <path
194
+ stroke-linecap="round"
195
+ stroke-linejoin="round"
196
+ stroke-width="2"
197
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
198
+ </svg>
199
+ </div>
200
+ <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">API Health</h4>
201
+ <p class="text-xs text-[var(--kyro-text-secondary)]">System status</p>
202
+ </a>
203
+ )}
204
+ </div>
171
205
  </div>
172
- </div>
206
+ )}
173
207
 
174
208
  <script is:inline define:vars={{ apiPath, adminPath }}>
175
209
  (function () {
210
+ function pruneDashboardRBAC(user, permissions) {
211
+ if (!user) {
212
+ user = window.__kyroAuth?.user;
213
+ permissions = window.__kyroAuth?.permissions;
214
+ }
215
+ if (!user) return;
216
+
217
+ var userRole = user.role || "";
218
+ var isSuperAdmin = userRole === "super_admin";
219
+ var isAdmin = userRole === "admin" || isSuperAdmin;
220
+
221
+ // Prune Quick Links
222
+ var quickLinkItems = document.querySelectorAll("[data-quick-link]");
223
+ var visibleQuickLinks = 0;
224
+ quickLinkItems.forEach(function (el) {
225
+ var slug = el.getAttribute("data-quick-link");
226
+ var hasCreate = isAdmin || (permissions && permissions.collections && permissions.collections[slug] && permissions.collections[slug].create === true);
227
+ if (!hasCreate) {
228
+ el.style.display = "none";
229
+ } else {
230
+ el.style.display = "";
231
+ visibleQuickLinks++;
232
+ }
233
+ });
234
+ var quickLinksSection = document.getElementById("dashboard-quick-links-section");
235
+ if (quickLinksSection) {
236
+ quickLinksSection.style.display = visibleQuickLinks > 0 ? "" : "none";
237
+ }
238
+
239
+ // Prune Security Cards
240
+ var secCards = document.querySelectorAll("[data-sec-card]");
241
+ var visibleSecCards = 0;
242
+ secCards.forEach(function (el) {
243
+ var card = el.getAttribute("data-sec-card");
244
+ var hasAccess = false;
245
+ if (card === "audit") {
246
+ hasAccess = isAdmin || (permissions && permissions.collections && permissions.collections.audit_logs && permissions.collections.audit_logs.read === true);
247
+ } else if (card === "locked-users") {
248
+ hasAccess = isAdmin || (permissions && permissions.collections && permissions.collections.users && permissions.collections.users.read === true);
249
+ } else if (card === "roles") {
250
+ hasAccess = isSuperAdmin;
251
+ } else if (card === "health") {
252
+ hasAccess = isAdmin;
253
+ }
254
+ if (!hasAccess) {
255
+ el.style.display = "none";
256
+ } else {
257
+ el.style.display = "";
258
+ visibleSecCards++;
259
+ }
260
+ });
261
+ var secSection = document.getElementById("dashboard-security-section");
262
+ if (secSection) {
263
+ secSection.style.display = visibleSecCards > 0 ? "" : "none";
264
+ }
265
+ }
266
+
267
+ window.addEventListener("kyro:auth-ready", function (e) {
268
+ var detail = e.detail || {};
269
+ pruneDashboardRBAC(detail.user, detail.permissions);
270
+ });
271
+
272
+ pruneDashboardRBAC();
176
273
  var searchInput = document.getElementById("header-search-input");
177
274
  var searchResults = document.getElementById("header-search-results");
178
275
  if (!searchInput || !searchResults) return;
@@ -245,5 +342,7 @@ const authItems = authCollections.map((slug) => ({
245
342
  });
246
343
  })();
247
344
  </script>
345
+ </>
346
+ )}
248
347
  </div>
249
348
  </AdminLayout>
@@ -3,8 +3,10 @@ import AdminLayout from "../layouts/AdminLayout.astro";
3
3
  import i18next from '../lib/i18n';
4
4
  import { ApiKeysManager } from "../components/ApiKeysManager";
5
5
 
6
- import { adminPath, apiPath } from '../lib/paths';
6
+ import { requireAuthServer } from '../lib/auth-server';
7
7
 
8
+ const redirect = await requireAuthServer(Astro.request, { requireAdmin: true });
9
+ if (redirect) return redirect;
8
10
  ---
9
11
 
10
12
  <AdminLayout title={i18next.t("pages." + "API Keys".replace(/ /g, ""), { defaultValue: "API Keys" })}>
@@ -3,8 +3,10 @@ import AdminLayout from '../layouts/AdminLayout.astro';
3
3
  import i18next from '../lib/i18n';
4
4
  import { MediaGallery } from '../components/MediaGallery';
5
5
 
6
- import { adminPath, apiPath } from '../lib/paths';
6
+ import { requireAuthServer } from '../lib/auth-server';
7
7
 
8
+ const redirect = await requireAuthServer(Astro.request, { collectionRead: 'media' });
9
+ if (redirect) return redirect;
8
10
  ---
9
11
 
10
12
  <AdminLayout title={i18next.t("pages." + "Media Library".replace(/ /g, ""), { defaultValue: "Media Library" })}>
@@ -18,13 +18,10 @@ let error = null;
18
18
 
19
19
  if (id) {
20
20
  try {
21
- const response = await fetch(
22
- `${Astro.url.origin}${apiPath}/${collection}/${id}`,
23
- {
24
- headers: Astro.request.headers,
25
- credentials: "include",
26
- },
27
- );
21
+ const response = await fetch(`${Astro.url.origin}${apiPath}/${collection}/${id}`, {
22
+ headers: Astro.request.headers,
23
+ credentials: "include",
24
+ });
28
25
  if (response.ok) {
29
26
  const result = await response.json();
30
27
  doc = result.data || null;
@@ -44,34 +41,19 @@ const pageTitle = doc
44
41
  <AdminLayout title={pageTitle}>
45
42
  <Fragment set:html={`<style>${richTextStyles}</style>`} />
46
43
  <div class="flex-1 overflow-y-auto space-y-6">
47
- <div class="surface-tile p-6 flex items-center justify-between">
44
+ <div class="surface-tile p-6 flex items-center justify-between rounded-lg">
48
45
  <div class="flex items-center gap-4">
49
46
  <a
50
47
  href={`${adminPath}/${collection}/${id}`}
51
48
  class="inline-flex items-center justify-center w-10 h-10 rounded-xl border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
52
49
  >
53
- <svg
54
- class="w-4 h-4"
55
- fill="none"
56
- stroke="currentColor"
57
- viewBox="0 0 24 24"
58
- >
59
- <path
60
- stroke-linecap="round"
61
- stroke-linejoin="round"
62
- stroke-width="2.5"
63
- d="M15 19l-7-7 7-7"></path>
50
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
51
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7"></path>
64
52
  </svg>
65
53
  </a>
66
54
  <div>
67
- <h1
68
- class="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]"
69
- >
70
- Preview Mode
71
- </h1>
72
- <p class="text-sm text-[var(--kyro-text-secondary)] mt-0.5">
73
- Showing draft content
74
- </p>
55
+ <h1 class="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">Preview Mode</h1>
56
+ <p class="text-sm text-[var(--kyro-text-secondary)] mt-0.5">Showing draft content</p>
75
57
  </div>
76
58
  </div>
77
59
  <div class="flex items-center gap-3">
@@ -110,51 +92,32 @@ const pageTitle = doc
110
92
  {field.label || field.name}
111
93
  </h3>
112
94
  <div class="text-sm text-[var(--kyro-text-primary)]">
113
- {field.type === "text" && (
114
- <p class="whitespace-pre-wrap">{value}</p>
115
- )}
116
- {field.type === "textarea" && (
117
- <p class="whitespace-pre-wrap">{value}</p>
118
- )}
119
- {field.type === "richtext" && (
120
- <div set:html={renderRichText(value)} />
121
- )}
95
+ {field.type === "text" && <p class="whitespace-pre-wrap">{value}</p>}
96
+ {field.type === "textarea" && <p class="whitespace-pre-wrap">{value}</p>}
97
+ {field.type === "richtext" && <div set:html={renderRichText(value)} />}
122
98
  {field.type === "markdown" && (
123
99
  <div class="prose prose-sm max-w-none">
124
- <pre class="whitespace-pre-wrap bg-transparent p-0">
125
- {value}
126
- </pre>
100
+ <pre class="whitespace-pre-wrap bg-transparent p-0">{value}</pre>
127
101
  </div>
128
102
  )}
129
103
  {field.type === "number" && <p>{value}</p>}
130
104
  {field.type === "boolean" && (
131
- <span class={value ? "text-green-400" : "text-red-400"}>
132
- {value ? "Yes" : "No"}
133
- </span>
105
+ <span class={value ? "text-green-400" : "text-red-400"}>{value ? "Yes" : "No"}</span>
134
106
  )}
135
107
  {field.type === "select" && (
136
- <span class="px-2 py-1 bg-[var(--kyro-surface-accent)] rounded text-xs">
137
- {value}
138
- </span>
139
- )}
140
- {field.type === "date" && (
141
- <p>{new Date(value).toLocaleString()}</p>
108
+ <span class="px-2 py-1 bg-[var(--kyro-surface-accent)] rounded text-xs">{value}</span>
142
109
  )}
110
+ {field.type === "date" && <p>{new Date(value).toLocaleString()}</p>}
143
111
  {field.type === "upload" && value.url && (
144
112
  <div>
145
- {value.url.match(
146
- /\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i,
147
- ) ? (
113
+ {value.url.match(/\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i) ? (
148
114
  <img
149
115
  src={value.url}
150
116
  alt={value.filename || "Image"}
151
117
  class="max-w-md rounded-lg border border-[var(--kyro-border)]"
152
118
  />
153
119
  ) : (
154
- <a
155
- href={value.url}
156
- class="text-blue-400 hover:underline"
157
- >
120
+ <a href={value.url} class="text-blue-400 hover:underline">
158
121
  {value.filename || "Download"}
159
122
  </a>
160
123
  )}
@@ -164,9 +127,7 @@ const pageTitle = doc
164
127
  <div class="grid grid-cols-4 gap-3">
165
128
  {value.map((item: any, idx: number) =>
166
129
  item?.url ? (
167
- item.url.match(
168
- /\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i,
169
- ) ? (
130
+ item.url.match(/\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i) ? (
170
131
  <img
171
132
  src={item.url}
172
133
  alt={item.filename || `Image ${idx + 1}`}
@@ -189,14 +150,8 @@ const pageTitle = doc
189
150
  .filter(([, v]) => v !== undefined)
190
151
  .map(([k, v]) => (
191
152
  <div class="flex gap-2">
192
- <span class="text-[var(--kyro-text-muted)]">
193
- {k}:
194
- </span>
195
- <span>
196
- {typeof v === "object"
197
- ? JSON.stringify(v)
198
- : String(v)}
199
- </span>
153
+ <span class="text-[var(--kyro-text-muted)]">{k}:</span>
154
+ <span>{typeof v === "object" ? JSON.stringify(v) : String(v)}</span>
200
155
  </div>
201
156
  ))}
202
157
  </div>
@@ -205,10 +160,7 @@ const pageTitle = doc
205
160
  )}
206
161
  {field.type === "relationship" && (
207
162
  <p class="text-[var(--kyro-text-secondary)]">
208
- {value.title ||
209
- value.name ||
210
- value.slug ||
211
- JSON.stringify(value)}
163
+ {value.title || value.name || value.slug || JSON.stringify(value)}
212
164
  </p>
213
165
  )}
214
166
  {field.type === "json" && (
@@ -217,9 +169,7 @@ const pageTitle = doc
217
169
  </pre>
218
170
  )}
219
171
  {field.type === "code" && (
220
- <pre class="text-xs bg-[var(--kyro-surface-accent)] p-3 rounded overflow-x-auto">
221
- {value}
222
- </pre>
172
+ <pre class="text-xs bg-[var(--kyro-surface-accent)] p-3 rounded overflow-x-auto">{value}</pre>
223
173
  )}
224
174
  {(!field.type || ["text", "string"].includes(field.type)) && (
225
175
  <p>{typeof value === "object" ? JSON.stringify(value) : value}</p>