@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
2
|
import { apiGet } from "../../lib/api";
|
|
3
|
-
import { useUIStore, toast } from "../../lib/stores";
|
|
3
|
+
import { useUIStore, useAuthStore, toast } from "../../lib/stores";
|
|
4
4
|
import { UploadField } from "../fields/UploadField";
|
|
5
5
|
import { useTranslation } from "react-i18next";
|
|
6
6
|
import { navigate } from '../../lib/navigate';
|
|
@@ -47,6 +47,27 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
47
47
|
const [, setDeleting] = useState(false);
|
|
48
48
|
const [locking, setLocking] = useState(false);
|
|
49
49
|
const [isLocked, setIsLocked] = useState(user.locked || false);
|
|
50
|
+
const [currentUserRole, setCurrentUserRole] = useState<string>("");
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if ((window as any).__kyroAuth?.user?.role) {
|
|
54
|
+
setCurrentUserRole((window as any).__kyroAuth.user.role);
|
|
55
|
+
} else {
|
|
56
|
+
apiGet<any>("/api/auth/me")
|
|
57
|
+
.then((res) => {
|
|
58
|
+
if (res?.user?.role) setCurrentUserRole(res.user.role);
|
|
59
|
+
})
|
|
60
|
+
.catch(() => { });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const handler = (e: any) => {
|
|
64
|
+
if (e.detail?.user?.role) {
|
|
65
|
+
setCurrentUserRole(e.detail.user.role);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener("kyro:auth-ready", handler);
|
|
69
|
+
return () => window.removeEventListener("kyro:auth-ready", handler);
|
|
70
|
+
}, []);
|
|
50
71
|
|
|
51
72
|
useEffect(() => {
|
|
52
73
|
if (typeof avatar === "string" && /^[0-9a-f-]+$/i.test(avatar)) {
|
|
@@ -105,6 +126,15 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
105
126
|
|
|
106
127
|
if (res.ok) {
|
|
107
128
|
toast.success("User updated");
|
|
129
|
+
const currentMe = (window as any).__kyroAuth?.user || useAuthStore.getState().user;
|
|
130
|
+
if (currentMe && (currentMe.id === user.id || currentMe.email === user.email)) {
|
|
131
|
+
apiGet<any>("/api/auth/me").then((meRes) => {
|
|
132
|
+
const updatedUser = meRes?.user || meRes;
|
|
133
|
+
if (updatedUser) {
|
|
134
|
+
useAuthStore.getState().setUser(updatedUser);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
108
138
|
navigate(adminPath + "/users");
|
|
109
139
|
} else {
|
|
110
140
|
toast.error(data.error || "Failed to save user");
|
|
@@ -182,7 +212,7 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
182
212
|
|
|
183
213
|
return (
|
|
184
214
|
<div className="flex-1 overflow-y-auto space-y-8">
|
|
185
|
-
<div className="surface-tile p-6 flex items-center justify-between">
|
|
215
|
+
<div className="surface-tile p-6 flex items-center justify-between rounded-lg">
|
|
186
216
|
<div className="flex items-center gap-4">
|
|
187
217
|
<div className="relative 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 overflow-hidden flex-shrink-0">
|
|
188
218
|
{avatarUrl ? (
|
|
@@ -200,23 +230,25 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
200
230
|
</p>
|
|
201
231
|
</div>
|
|
202
232
|
</div>
|
|
203
|
-
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
{(currentUserRole === "admin" || currentUserRole === "super_admin") && (
|
|
234
|
+
<div className="flex gap-2">
|
|
235
|
+
<button
|
|
236
|
+
onClick={handleLockToggle}
|
|
237
|
+
className="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"
|
|
238
|
+
>
|
|
239
|
+
{isLocked ? "Unlock User" : "Lock User"}
|
|
240
|
+
</button>
|
|
241
|
+
<button
|
|
242
|
+
onClick={handleDelete}
|
|
243
|
+
className="px-4 py-2 border border-red-200 rounded-xl text-sm font-bold text-red-600 hover:bg-red-50 transition-colors"
|
|
244
|
+
>
|
|
245
|
+
Delete
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
217
249
|
</div>
|
|
218
250
|
|
|
219
|
-
<div className="surface-tile p-6">
|
|
251
|
+
<div className="surface-tile p-6 rounded-lg">
|
|
220
252
|
<h2 className="text-lg font-bold text-[var(--kyro-text-primary)] tracking-tighter mb-6">
|
|
221
253
|
Details
|
|
222
254
|
</h2>
|
|
@@ -242,20 +274,33 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
242
274
|
</p>
|
|
243
275
|
</div>
|
|
244
276
|
<div>
|
|
245
|
-
<label className="text-xs font-bold text-[var(--kyro-text-secondary)]
|
|
277
|
+
<label className="text-xs font-bold text-[var(--kyro-text-secondary)] tracking-wider">
|
|
246
278
|
Role
|
|
247
279
|
</label>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
280
|
+
{(() => {
|
|
281
|
+
const isSuperAdmin = currentUserRole === "super_admin";
|
|
282
|
+
return (
|
|
283
|
+
<div>
|
|
284
|
+
<select
|
|
285
|
+
value={role}
|
|
286
|
+
disabled={!isSuperAdmin}
|
|
287
|
+
onChange={(e) => setRole(e.target.value)}
|
|
288
|
+
className="mt-1 w-full px-3 py-2 border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] rounded-lg text-sm font-medium text-[var(--kyro-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] disabled:opacity-60 disabled:cursor-not-allowed"
|
|
289
|
+
>
|
|
290
|
+
{roleOptions.map((r) => (
|
|
291
|
+
<option key={r} value={r}>
|
|
292
|
+
{r}
|
|
293
|
+
</option>
|
|
294
|
+
))}
|
|
295
|
+
</select>
|
|
296
|
+
{!isSuperAdmin && (
|
|
297
|
+
<p className="text-[11px] text-[var(--kyro-text-secondary)] mt-1 font-medium">
|
|
298
|
+
Only Super Admin can modify user roles.
|
|
299
|
+
</p>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
})()}
|
|
259
304
|
</div>
|
|
260
305
|
<div>
|
|
261
306
|
<label className="text-xs font-bold text-[var(--kyro-text-secondary)] tracking-wider">
|
|
@@ -89,7 +89,7 @@ export function UserForm({ mode, apiPath, adminPath, user }: UserFormProps) {
|
|
|
89
89
|
|
|
90
90
|
return (
|
|
91
91
|
<div className="flex-1 overflow-y-auto space-y-8">
|
|
92
|
-
<div className="surface-tile p-6 flex items-center justify-between">
|
|
92
|
+
<div className="surface-tile p-6 flex items-center justify-between rounded-lg">
|
|
93
93
|
<div>
|
|
94
94
|
<h1 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
|
|
95
95
|
{mode === "create" ? "Create User" : "Edit User"}
|
|
@@ -84,7 +84,7 @@ export function UsersList({
|
|
|
84
84
|
|
|
85
85
|
return (
|
|
86
86
|
<div className="flex-1 overflow-y-auto space-y-8">
|
|
87
|
-
<div className="surface-tile p-6 flex items-center justify-between">
|
|
87
|
+
<div className="surface-tile p-6 flex items-center justify-between rounded-lg">
|
|
88
88
|
<div>
|
|
89
89
|
<h1 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
|
|
90
90
|
Users
|
|
@@ -117,7 +117,7 @@ export function UsersList({
|
|
|
117
117
|
</a>
|
|
118
118
|
</div>
|
|
119
119
|
|
|
120
|
-
<div className="surface-tile overflow-hidden">
|
|
120
|
+
<div className="surface-tile overflow-hidden rounded-lg">
|
|
121
121
|
{users.length === 0 ? (
|
|
122
122
|
<div className="px-8 py-16 text-center">
|
|
123
123
|
<div className="flex flex-col items-center gap-4">
|
package/src/integration.ts
CHANGED
|
@@ -317,6 +317,10 @@ module.exports = debug;
|
|
|
317
317
|
{ pattern: "", entrypoint: "./pages/index.astro" },
|
|
318
318
|
{ pattern: "/login", entrypoint: "./pages/auth/login.astro" },
|
|
319
319
|
{ pattern: "/register", entrypoint: "./pages/auth/register.astro" },
|
|
320
|
+
{ pattern: "/auth/check-email", entrypoint: "./pages/auth/check-email.astro" },
|
|
321
|
+
{ pattern: "/auth/forgot-password", entrypoint: "./pages/auth/forgot-password.astro" },
|
|
322
|
+
{ pattern: "/auth/reset-password", entrypoint: "./pages/auth/reset-password.astro" },
|
|
323
|
+
{ pattern: "/auth/verify-email", entrypoint: "./pages/auth/verify-email.astro" },
|
|
320
324
|
{ pattern: "/media", entrypoint: "./pages/media.astro" },
|
|
321
325
|
{ pattern: "/users", entrypoint: "./pages/users/index.astro" },
|
|
322
326
|
{ pattern: "/users/new", entrypoint: "./pages/users/new.astro" },
|
|
@@ -112,50 +112,42 @@ const defaultLang = siteSettings?.defaultLanguage || "en";
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// Navigation Guard - Check if user has read access to current page
|
|
115
|
-
if (permissions) {
|
|
115
|
+
if (permissions && user) {
|
|
116
116
|
const path = window.location.pathname;
|
|
117
117
|
const relativePath = path.replace(adminPath, "");
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
const userRole = user.role || "";
|
|
119
|
+
const isSuperAdmin = userRole === "super_admin";
|
|
120
|
+
const isAdmin = userRole === "admin" || isSuperAdmin;
|
|
121
|
+
|
|
122
|
+
let hasAccess = true;
|
|
123
|
+
|
|
124
|
+
if (relativePath.startsWith("/roles")) {
|
|
125
|
+
if (!isSuperAdmin) hasAccess = false;
|
|
126
|
+
} else if (relativePath.startsWith("/plugins") || relativePath.startsWith("/keys") || relativePath.startsWith("/webhooks")) {
|
|
127
|
+
if (!isAdmin) hasAccess = false;
|
|
128
|
+
} else if (relativePath.startsWith("/users")) {
|
|
129
|
+
if (!isAdmin && permissions.collections?.users?.read !== true) hasAccess = false;
|
|
126
130
|
} else if (relativePath.startsWith("/audit")) {
|
|
127
|
-
|
|
128
|
-
type = "collection";
|
|
131
|
+
if (!isAdmin && permissions.collections?.audit_logs?.read !== true) hasAccess = false;
|
|
129
132
|
} else if (relativePath.startsWith("/media")) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
slug = relativePath.split("/")[2];
|
|
134
|
-
type = "global";
|
|
133
|
+
if (!isAdmin && permissions.collections?.media?.read !== true) hasAccess = false;
|
|
134
|
+
} else if (relativePath.startsWith("/settings")) {
|
|
135
|
+
if (!isAdmin) hasAccess = false;
|
|
135
136
|
} else if (
|
|
136
137
|
relativePath.includes("/") &&
|
|
137
138
|
!relativePath.startsWith("/login") &&
|
|
138
|
-
!relativePath.startsWith("/403")
|
|
139
|
+
!relativePath.startsWith("/403") &&
|
|
140
|
+
!relativePath.startsWith("/auth/")
|
|
139
141
|
) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (slug && type) {
|
|
146
|
-
let hasAccess = true;
|
|
147
|
-
if (type === "collection" && permissions.collections) {
|
|
148
|
-
const p = permissions.collections[slug];
|
|
149
|
-
if (p && p.read === false) hasAccess = false;
|
|
150
|
-
} else if (type === "global" && permissions.globals) {
|
|
151
|
-
const p = permissions.globals[slug];
|
|
152
|
-
if (p && p.read === false) hasAccess = false;
|
|
142
|
+
const colSlug = relativePath.split("/")[1];
|
|
143
|
+
if (!isAdmin && colSlug && permissions.collections?.[colSlug]?.read !== true) {
|
|
144
|
+
hasAccess = false;
|
|
153
145
|
}
|
|
146
|
+
}
|
|
154
147
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
148
|
+
if (!hasAccess) {
|
|
149
|
+
console.log("[AdminLayout] Access denied for", relativePath, "redirecting to 403");
|
|
150
|
+
window.location.href = adminPath + "/403";
|
|
159
151
|
}
|
|
160
152
|
}
|
|
161
153
|
} catch (err) {
|
|
@@ -61,16 +61,25 @@ const defaultLang = siteSettings?.defaultLanguage || "en";
|
|
|
61
61
|
/>
|
|
62
62
|
<script is:inline define:vars={{ adminPath, apiPath }}>
|
|
63
63
|
(async () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
const pathname = window.location.pathname;
|
|
65
|
+
const isAuthActionPage =
|
|
66
|
+
pathname.includes("/auth/check-email") ||
|
|
67
|
+
pathname.includes("/auth/verify-email") ||
|
|
68
|
+
pathname.includes("/auth/reset-password") ||
|
|
69
|
+
pathname.includes("/auth/forgot-password");
|
|
70
|
+
|
|
71
|
+
if (!isAuthActionPage) {
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(apiPath + "/auth/me", { credentials: "include" });
|
|
74
|
+
if (res.ok) {
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
if (data?.user) {
|
|
77
|
+
window.location.href = adminPath;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
71
80
|
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
74
83
|
const getTheme = () => {
|
|
75
84
|
const stored = localStorage.getItem("theme");
|
|
76
85
|
if (stored) return stored;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { apiPath, adminPath } from "./paths";
|
|
2
|
+
|
|
3
|
+
export interface AuthServerResult {
|
|
4
|
+
user: any | null;
|
|
5
|
+
permissions: any | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RequireAuthOptions {
|
|
9
|
+
requireAdmin?: boolean;
|
|
10
|
+
requireSuperAdmin?: boolean;
|
|
11
|
+
collectionRead?: string;
|
|
12
|
+
collectionCreate?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolves authenticated user and permissions directly on the server during Astro SSR requests.
|
|
17
|
+
* Uses dynamic apiPath to support custom API endpoint configurations.
|
|
18
|
+
*/
|
|
19
|
+
export async function getAuthServer(request: Request): Promise<AuthServerResult> {
|
|
20
|
+
try {
|
|
21
|
+
const cookie = request.headers.get("cookie") || "";
|
|
22
|
+
if (!cookie) return { user: null, permissions: null };
|
|
23
|
+
|
|
24
|
+
const url = new URL(request.url);
|
|
25
|
+
const origin = url.origin;
|
|
26
|
+
const cleanApiPath = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
|
|
27
|
+
|
|
28
|
+
const [meRes, accessRes] = await Promise.all([
|
|
29
|
+
fetch(`${origin}${cleanApiPath}/auth/me`, {
|
|
30
|
+
headers: { cookie },
|
|
31
|
+
}).catch(() => null),
|
|
32
|
+
fetch(`${origin}${cleanApiPath}/auth/access`, {
|
|
33
|
+
headers: { cookie },
|
|
34
|
+
}).catch(() => null),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const meData = meRes && meRes.ok ? await meRes.json() : null;
|
|
38
|
+
const permissions = accessRes && accessRes.ok ? await accessRes.json() : null;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
user: meData?.user || null,
|
|
42
|
+
permissions,
|
|
43
|
+
};
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error("[getAuthServer] Error resolving SSR auth:", err);
|
|
46
|
+
return { user: null, permissions: null };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Declarative server-side SSR route guard helper.
|
|
52
|
+
* Returns a 302 HTTP Response redirect if the authenticated user lacks permissions,
|
|
53
|
+
* or null if access is authorized.
|
|
54
|
+
*/
|
|
55
|
+
export async function requireAuthServer(
|
|
56
|
+
request: Request,
|
|
57
|
+
options?: RequireAuthOptions
|
|
58
|
+
): Promise<Response | null> {
|
|
59
|
+
const { user, permissions } = await getAuthServer(request);
|
|
60
|
+
if (!user) return null;
|
|
61
|
+
|
|
62
|
+
const isSuperAdmin = user.role === "super_admin";
|
|
63
|
+
const isAdmin = user.role === "admin" || isSuperAdmin;
|
|
64
|
+
const isCustomer = user.role === "customer";
|
|
65
|
+
|
|
66
|
+
let hasAccess = true;
|
|
67
|
+
|
|
68
|
+
if (options?.requireSuperAdmin && !isSuperAdmin) {
|
|
69
|
+
hasAccess = false;
|
|
70
|
+
} else if (options?.requireAdmin && !isAdmin) {
|
|
71
|
+
hasAccess = false;
|
|
72
|
+
} else if (options?.collectionRead) {
|
|
73
|
+
const col = options.collectionRead;
|
|
74
|
+
if ((col === "settings" || (col === "media" && isCustomer)) && !isAdmin) {
|
|
75
|
+
hasAccess = false;
|
|
76
|
+
} else if (col !== "settings") {
|
|
77
|
+
const url = new URL(request.url);
|
|
78
|
+
const isSelfUserAccess = col === "users" && !!user?.id && url.pathname.endsWith(`/users/${user.id}`);
|
|
79
|
+
const canRead = isAdmin || isSelfUserAccess || permissions?.collections?.[col]?.read === true;
|
|
80
|
+
if (!canRead) hasAccess = false;
|
|
81
|
+
}
|
|
82
|
+
} else if (options?.collectionCreate) {
|
|
83
|
+
const col = options.collectionCreate;
|
|
84
|
+
if (col === "settings" && !isAdmin) {
|
|
85
|
+
hasAccess = false;
|
|
86
|
+
} else if (col !== "settings") {
|
|
87
|
+
const canCreate = isAdmin || permissions?.collections?.[col]?.create === true;
|
|
88
|
+
if (!canCreate) hasAccess = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!hasAccess) {
|
|
93
|
+
const url = new URL(request.url);
|
|
94
|
+
const targetUrl = `${url.origin}${adminPath}/403`;
|
|
95
|
+
return Response.redirect(targetUrl, 302);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
}
|
package/src/pages/403.astro
CHANGED
|
@@ -1,66 +1,85 @@
|
|
|
1
1
|
---
|
|
2
2
|
import AdminLayout from "../layouts/AdminLayout.astro";
|
|
3
|
-
import i18next from
|
|
4
|
-
import { adminPath } from "../lib/paths";
|
|
3
|
+
import i18next from "../lib/i18n";
|
|
4
|
+
import { adminPath, apiPath } from "../lib/paths";
|
|
5
|
+
import { getAuthServer } from "../lib/auth-server";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const { user } = await getAuthServer(Astro.request);
|
|
8
|
+
const userEmail = user?.email || "";
|
|
9
|
+
const userRole = user?.role || "";
|
|
7
10
|
---
|
|
8
11
|
|
|
9
|
-
<AdminLayout title={i18next.t("pages." + "Access Denied".replace(/ /g, ""), { defaultValue: "Access Denied" })}>
|
|
10
|
-
<div class="flex-1 flex items-center justify-center p-
|
|
11
|
-
<div
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
<AdminLayout title={i18next.t("pages." + "Access Denied".replace(/ /g, ""), { defaultValue: "Access Denied" })}>
|
|
13
|
+
<div class="flex-1 flex items-center justify-center p-6 min-h-[calc(100vh-120px)]">
|
|
14
|
+
<div
|
|
15
|
+
class="surface-tile max-w-md w-full p-8 rounded-2xl border border-[var(--kyro-border)] shadow-sm text-center space-y-6"
|
|
16
|
+
>
|
|
17
|
+
<!-- Icon & Status Tag -->
|
|
18
|
+
<div class="flex flex-col items-center gap-3">
|
|
14
19
|
<div
|
|
15
|
-
class="
|
|
20
|
+
class="w-12 h-12 rounded-xl bg-red-500/10 border border-red-500/20 text-red-500 flex items-center justify-center"
|
|
16
21
|
>
|
|
17
|
-
<svg
|
|
18
|
-
class="w-12 h-12"
|
|
19
|
-
fill="none"
|
|
20
|
-
stroke="currentColor"
|
|
21
|
-
viewBox="0 0 24 24"
|
|
22
|
-
>
|
|
22
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
23
23
|
<path
|
|
24
24
|
stroke-linecap="round"
|
|
25
25
|
stroke-linejoin="round"
|
|
26
|
-
stroke-width="
|
|
26
|
+
stroke-width="1.75"
|
|
27
27
|
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"
|
|
28
28
|
></path>
|
|
29
29
|
</svg>
|
|
30
30
|
</div>
|
|
31
|
+
<span
|
|
32
|
+
class="text-[10px] font-mono font-semibold tracking-widest uppercase px-3 py-1 bg-red-500/10 text-rose-500 rounded-full border border-red-500/20"
|
|
33
|
+
>
|
|
34
|
+
HTTP 403 · Restricted
|
|
35
|
+
</span>
|
|
31
36
|
</div>
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
>
|
|
37
|
-
|
|
38
|
-
</h1>
|
|
39
|
-
<p class="text-[var(--kyro-text-secondary)] font-medium">
|
|
40
|
-
You don't have the required permissions to access this page. Please
|
|
41
|
-
contact your administrator if you believe this is an error.
|
|
38
|
+
<!-- Text Header -->
|
|
39
|
+
<div class="space-y-1.5">
|
|
40
|
+
<h1 class="text-base font-semibold text-[var(--kyro-text-primary)]">Access Restricted</h1>
|
|
41
|
+
<p class="text-xs text-[var(--kyro-text-secondary)] leading-relaxed max-w-xs mx-auto">
|
|
42
|
+
Your current account role does not have permission to view or manage this resource.
|
|
42
43
|
</p>
|
|
43
44
|
</div>
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
<!-- Account Context Badge -->
|
|
47
|
+
{
|
|
48
|
+
userRole && (
|
|
49
|
+
<div class="p-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg text-left flex items-center justify-between text-xs">
|
|
50
|
+
<div class="min-w-0 pr-2">
|
|
51
|
+
<p class="text-[10px] capitalise font-bold tracking-wider text-[var(--kyro-text-muted)]">Signed In As</p>
|
|
52
|
+
<p class="font-medium text-[var(--kyro-text-primary)] truncate">{userEmail || "Authenticated User"}</p>
|
|
53
|
+
</div>
|
|
54
|
+
<span class="px-2.5 py-1 text-[10px] font-bold rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] capitalize tracking-wider flex-shrink-0">
|
|
55
|
+
{userRole.replace("_", " ")}
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
<!-- Action Buttons -->
|
|
62
|
+
<div class="pt-1 flex items-center justify-center gap-2">
|
|
63
|
+
<button
|
|
64
|
+
onclick="window.history.back()"
|
|
65
|
+
class="px-4 py-2 text-xs font-semibold rounded-xl border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] transition-colors cursor-pointer"
|
|
66
|
+
>
|
|
67
|
+
Go Back
|
|
68
|
+
</button>
|
|
69
|
+
|
|
46
70
|
<a
|
|
47
71
|
href={adminPath}
|
|
48
|
-
class="
|
|
72
|
+
class="px-4 py-2 text-xs font-semibold rounded-xl bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-opacity inline-flex items-center gap-1.5"
|
|
49
73
|
>
|
|
50
|
-
<svg
|
|
51
|
-
class="w-4 h-4"
|
|
52
|
-
fill="none"
|
|
53
|
-
stroke="currentColor"
|
|
54
|
-
viewBox="0 0 24 24"
|
|
55
|
-
>
|
|
74
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
56
75
|
<path
|
|
57
76
|
stroke-linecap="round"
|
|
58
77
|
stroke-linejoin="round"
|
|
59
|
-
stroke-width="2
|
|
78
|
+
stroke-width="2"
|
|
60
79
|
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
|
61
80
|
></path>
|
|
62
81
|
</svg>
|
|
63
|
-
|
|
82
|
+
Dashboard
|
|
64
83
|
</a>
|
|
65
84
|
</div>
|
|
66
85
|
</div>
|
|
@@ -25,6 +25,14 @@ if (!isSettings) {
|
|
|
25
25
|
: `Edit ${config.singularLabel || config.label || collection}`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
import { requireAuthServer } from "../../lib/auth-server";
|
|
29
|
+
|
|
30
|
+
if (collection) {
|
|
31
|
+
const reqOp = (!id || id === "new") ? "collectionCreate" : "collectionRead";
|
|
32
|
+
const redirect = await requireAuthServer(Astro.request, { [reqOp]: collection });
|
|
33
|
+
if (redirect) return redirect;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
import SettingsPageAstro from "../settings/[slug].astro";
|
|
29
37
|
---
|
|
30
38
|
|
|
@@ -21,6 +21,13 @@ if (!isSettings) {
|
|
|
21
21
|
config = collections[collection];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
import { requireAuthServer } from "../../lib/auth-server";
|
|
25
|
+
|
|
26
|
+
if (collection) {
|
|
27
|
+
const redirect = await requireAuthServer(Astro.request, { collectionRead: collection });
|
|
28
|
+
if (redirect) return redirect;
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
import SettingsIndexAstro from "../settings/index.astro";
|
|
25
32
|
|
|
26
33
|
let docs: any[] = [];
|
|
@@ -3,7 +3,10 @@ import AdminLayout from '../../layouts/AdminLayout.astro';
|
|
|
3
3
|
import i18next from '../../lib/i18n';
|
|
4
4
|
import { AuditLogsPage } from '../../components/AuditLogsPage';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { requireAuthServer } from '../../lib/auth-server';
|
|
7
|
+
|
|
8
|
+
const redirect = await requireAuthServer(Astro.request, { collectionRead: 'audit_logs' });
|
|
9
|
+
if (redirect) return redirect;
|
|
7
10
|
---
|
|
8
11
|
|
|
9
12
|
<AdminLayout title={i18next.t("pages." + "Audit Logs".replace(/ /g, ""), { defaultValue: "Audit Logs" })}>
|