@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.
- package/dist/index.cjs +24 -24
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +19 -19
- package/package.json +6 -6
- package/src/components/ApiKeysManager.tsx +177 -227
- package/src/components/AuditLogsPage.tsx +7 -7
- package/src/components/AuthorDashboard.tsx +235 -0
- package/src/components/AutoForm.tsx +14 -4
- package/src/components/CustomerDashboard.tsx +213 -0
- package/src/components/Dashboard.tsx +1 -1
- package/src/components/DashboardMetrics.tsx +74 -9
- package/src/components/DashboardQuickSections.tsx +237 -0
- package/src/components/ListView.tsx +3 -3
- package/src/components/MediaGallery.tsx +2 -2
- package/src/components/PluginsManager.tsx +1 -1
- package/src/components/SessionsManager.tsx +15 -15
- package/src/components/Sidebar.astro +564 -116
- package/src/components/UserManagement.tsx +43 -9
- package/src/components/UserMenu.tsx +123 -61
- package/src/components/autoform/AutoFormEditView.tsx +4 -4
- package/src/components/autoform/AutoFormHeader.tsx +2 -2
- package/src/components/ui/CommandPalette.tsx +1 -1
- package/src/components/ui/Dropdown.tsx +71 -16
- package/src/components/ui/PageHeader.tsx +1 -1
- package/src/components/ui/Pagination.tsx +1 -1
- package/src/components/ui/Toast.tsx +3 -2
- package/src/components/ui/Toaster.tsx +1 -1
- package/src/components/users/UserDetail.tsx +74 -29
- package/src/components/users/UserForm.tsx +1 -1
- package/src/components/users/UsersList.tsx +2 -2
- package/src/integration.ts +4 -0
- package/src/layouts/AdminLayout.astro +26 -34
- package/src/layouts/AuthLayout.astro +18 -9
- package/src/lib/auth-server.ts +99 -0
- package/src/pages/403.astro +54 -35
- package/src/pages/[collection]/[id].astro +8 -0
- package/src/pages/[collection]/index.astro +7 -0
- package/src/pages/audit/index.astro +4 -1
- package/src/pages/auth/check-email.astro +146 -0
- package/src/pages/auth/forgot-password.astro +95 -0
- package/src/pages/auth/login.astro +29 -12
- package/src/pages/auth/register.astro +38 -12
- package/src/pages/auth/reset-password.astro +125 -0
- package/src/pages/auth/verify-email.astro +87 -0
- package/src/pages/index.astro +220 -121
- package/src/pages/keys.astro +3 -1
- package/src/pages/media.astro +3 -1
- package/src/pages/preview/[collection]/[id].astro +23 -73
- package/src/pages/roles/index.astro +134 -101
- package/src/pages/settings/[slug].astro +43 -3
- package/src/pages/users/[id].astro +5 -1
- package/src/pages/users/index.astro +4 -0
- package/src/pages/users/new.astro +4 -0
- package/src/pages/webhooks.astro +3 -1
- package/src/routes.ts +16 -0
package/src/pages/index.astro
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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:
|
|
55
|
-
{(!!collections["orders"] || !!collections["products"]) && <RevenueChart client:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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>
|
package/src/pages/keys.astro
CHANGED
|
@@ -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 {
|
|
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" })}>
|
package/src/pages/media.astro
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|