@kyro-cms/admin 0.5.3 → 0.5.5
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/{EditorClient-YLCGVDXY.cjs → EditorClient-Q23UXR37.cjs} +14 -14
- package/dist/{EditorClient-XEUOVAAC.js → EditorClient-T5PASFNR.js} +2 -2
- package/dist/chunk-3BGDYKTD.cjs +348 -0
- package/dist/chunk-3BGDYKTD.cjs.map +1 -0
- package/dist/chunk-EEFXLQVT.js +3 -0
- package/dist/chunk-EEFXLQVT.js.map +1 -0
- package/dist/index.cjs +462 -1020
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +13 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +271 -829
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
- package/src/components/AuditLogsPage.tsx +4 -8
- package/src/components/Dashboard.tsx +2 -1
- package/src/components/DetailView.tsx +9 -2
- package/src/components/ListView.tsx +3 -2
- package/src/components/MediaGallery.tsx +13 -6
- package/src/components/Sidebar.astro +1 -1
- package/src/components/ui/Shimmer.tsx +28 -0
- package/src/components/users/UserDetail.tsx +1 -1
- package/src/components/users/UserForm.tsx +1 -1
- package/src/components/users/UsersList.tsx +1 -1
- package/src/hooks/useAutoFormState.ts +19 -3
- package/src/integration.ts +77 -25
- package/src/layouts/AdminLayout.astro +70 -48
- package/src/lib/config.ts +6 -1
- package/src/lib/globals.ts +56 -20
- package/src/pages/index.astro +1 -1
- package/src/pages/roles/index.astro +1 -1
- package/src/pages/users/[id].astro +2 -2
- package/src/styles/main.css +17 -0
- package/dist/chunk-7KPIUCGT.js +0 -384
- package/dist/chunk-7KPIUCGT.js.map +0 -1
- package/dist/chunk-GOACG6R7.cjs +0 -473
- package/dist/chunk-GOACG6R7.cjs.map +0 -1
- /package/dist/{EditorClient-XEUOVAAC.js.map → EditorClient-Q23UXR37.cjs.map} +0 -0
- /package/dist/{EditorClient-YLCGVDXY.cjs.map → EditorClient-T5PASFNR.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyro-cms/admin",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -89,30 +89,34 @@
|
|
|
89
89
|
"@uiw/codemirror-theme-github": "^4.25.9",
|
|
90
90
|
"@uiw/react-codemirror": "^4.25.9",
|
|
91
91
|
"astro": "^6.3.1",
|
|
92
|
+
"astro-loading-indicator": "^0.8.0",
|
|
93
|
+
"dotenv": "^17.4.2",
|
|
92
94
|
"graphiql": "^5.2.2",
|
|
93
95
|
"idb-keyval": "^6.2.2",
|
|
94
96
|
"lucide-react": "^0.475.0",
|
|
95
97
|
"react": "^19.0.0",
|
|
96
98
|
"react-dom": "^19.0.0",
|
|
99
|
+
"react-icons": "^5.0.0",
|
|
97
100
|
"react-image-crop": "^11.0.10",
|
|
98
101
|
"slate": "^0.124.1",
|
|
99
102
|
"slate-history": "^0.113.1",
|
|
100
103
|
"slate-react": "^0.124.0",
|
|
104
|
+
"swup": "^4.9.0",
|
|
101
105
|
"tailwindcss": "^4.0.0",
|
|
106
|
+
"unstorage": "^1.17.5",
|
|
102
107
|
"zustand": "^5.0.3"
|
|
103
108
|
},
|
|
104
109
|
"devDependencies": {
|
|
105
110
|
"@astrojs/check": "^0.9.9",
|
|
106
111
|
"@types/react": "^19.0.0",
|
|
107
112
|
"@types/react-dom": "^19.0.0",
|
|
108
|
-
"dotenv": "^17.4.2",
|
|
109
113
|
"dotenv-cli": "^11.0.0",
|
|
110
114
|
"tsup": "^6.0.0",
|
|
111
115
|
"typescript": "^6.0.3",
|
|
112
116
|
"vitest": "^4.1.4"
|
|
113
117
|
},
|
|
114
118
|
"peerDependencies": {
|
|
115
|
-
"@kyro-cms/core": "^0.5.
|
|
119
|
+
"@kyro-cms/core": "^0.5.4",
|
|
116
120
|
"react": "^18.0.0",
|
|
117
121
|
"react-dom": "^18.0.0"
|
|
118
122
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { fetchWithAuth } from "../lib/api";
|
|
3
3
|
import { Modal } from "./ui/Modal";
|
|
4
|
+
import { Shimmer } from "./ui/Shimmer";
|
|
4
5
|
|
|
5
6
|
interface AuditLog {
|
|
6
7
|
id: string;
|
|
@@ -206,7 +207,7 @@ export function AuditLogsPage() {
|
|
|
206
207
|
};
|
|
207
208
|
|
|
208
209
|
return (
|
|
209
|
-
<div className="flex-1 overflow-y-auto
|
|
210
|
+
<div className="flex-1 overflow-y-auto space-y-6">
|
|
210
211
|
{/* Header */}
|
|
211
212
|
<div className="surface-tile p-6 flex items-center justify-between gap-8">
|
|
212
213
|
<div>
|
|
@@ -332,13 +333,8 @@ export function AuditLogsPage() {
|
|
|
332
333
|
{/* Table */}
|
|
333
334
|
<div className="surface-tile overflow-hidden">
|
|
334
335
|
{loading ? (
|
|
335
|
-
<div className="
|
|
336
|
-
|
|
337
|
-
<div
|
|
338
|
-
key={i}
|
|
339
|
-
className="h-16 animate-pulse bg-[var(--kyro-surface-accent)]"
|
|
340
|
-
/>
|
|
341
|
-
))}
|
|
336
|
+
<div className="space-y-2 p-4">
|
|
337
|
+
<Shimmer variant="table-row" count={5} />
|
|
342
338
|
</div>
|
|
343
339
|
) : logs.length === 0 ? (
|
|
344
340
|
<div className="px-8 py-20 text-center">
|
|
@@ -3,6 +3,7 @@ import { LayoutDashboard, FileText, Image as ImageIcon, Users, Plus, ArrowUpRigh
|
|
|
3
3
|
import { useAuthStore } from "../lib/stores";
|
|
4
4
|
import { authCollectionSlugs } from "../lib/config";
|
|
5
5
|
import { PageHeader } from "./ui/PageHeader";
|
|
6
|
+
import { Shimmer } from "./ui/Shimmer";
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
interface DashboardProps {
|
|
@@ -124,7 +125,7 @@ export function Dashboard({ collections, onNavigate, user }: DashboardProps) {
|
|
|
124
125
|
{stat.label}
|
|
125
126
|
</p>
|
|
126
127
|
<h3 className="text-3xl font-bold tracking-tighter">
|
|
127
|
-
{loading ? "
|
|
128
|
+
{loading ? <Shimmer variant="text" className="w-16" /> : stat.value}
|
|
128
129
|
</h3>
|
|
129
130
|
</div>
|
|
130
131
|
<div
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { AutoForm } from "./AutoForm";
|
|
9
9
|
import { ActionBar, type DocumentStatus, type SaveStatus } from "./ActionBar";
|
|
10
10
|
import { Spinner } from "./ui/Spinner";
|
|
11
|
+
import { Shimmer } from "./ui/Shimmer";
|
|
11
12
|
import { useToast } from "./ui/Toast";
|
|
12
13
|
import { useUIStore } from "../lib/stores";
|
|
13
14
|
import { PageHeader } from "./ui/PageHeader";
|
|
@@ -233,8 +234,14 @@ export function DetailView({
|
|
|
233
234
|
if (loading) {
|
|
234
235
|
return (
|
|
235
236
|
<div className="kyro-detail">
|
|
236
|
-
<div className="
|
|
237
|
-
<
|
|
237
|
+
<div className="space-y-6 p-4">
|
|
238
|
+
<div className="space-y-2">
|
|
239
|
+
<Shimmer variant="text" className="w-1/3" />
|
|
240
|
+
<Shimmer variant="text" className="w-2/3" />
|
|
241
|
+
</div>
|
|
242
|
+
<div className="space-y-4">
|
|
243
|
+
<Shimmer variant="rect" count={4} />
|
|
244
|
+
</div>
|
|
238
245
|
</div>
|
|
239
246
|
</div>
|
|
240
247
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
|
2
2
|
import { Spinner } from "./ui/Spinner";
|
|
3
|
+
import { Shimmer } from "./ui/Shimmer";
|
|
3
4
|
import { Plus } from "./ui/icons";
|
|
4
5
|
import { apiGet, apiDelete, withCacheBust } from "../lib/api";
|
|
5
6
|
|
|
@@ -615,8 +616,8 @@ export function ListView({
|
|
|
615
616
|
{/* Data Table */}
|
|
616
617
|
<div className="surface-tile overflow-hidden">
|
|
617
618
|
{loading ? (
|
|
618
|
-
<div className="
|
|
619
|
-
<
|
|
619
|
+
<div className="space-y-2 p-4">
|
|
620
|
+
<Shimmer variant="table-row" count={8} />
|
|
620
621
|
</div>
|
|
621
622
|
) : docs.length === 0 ? (
|
|
622
623
|
<div className="flex flex-col items-center justify-center py-16 px-8">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { Spinner } from "./ui/Spinner";
|
|
4
|
+
import { Shimmer } from "./ui/Shimmer";
|
|
4
5
|
import { SlidePanel } from "./ui/SlidePanel";
|
|
5
6
|
import { Badge } from "./ui/Badge";
|
|
6
7
|
import { Folder } from "./ui/icons";
|
|
@@ -139,9 +140,8 @@ export function MediaGallery({
|
|
|
139
140
|
{},
|
|
140
141
|
);
|
|
141
142
|
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
|
142
|
-
const [storageConfigured, setStorageConfigured] = useState<boolean | null>(
|
|
143
|
-
|
|
144
|
-
);
|
|
143
|
+
const [storageConfigured, setStorageConfigured] = useState<boolean | null>(null);
|
|
144
|
+
const [storageChecked, setStorageChecked] = useState(false);
|
|
145
145
|
const [showStorageConfigModal, setShowStorageConfigModal] = useState(false);
|
|
146
146
|
const [page, setPage] = useState(1);
|
|
147
147
|
const [total, setTotal] = useState(0);
|
|
@@ -204,6 +204,13 @@ export function MediaGallery({
|
|
|
204
204
|
checkStorage();
|
|
205
205
|
}, [checkStorage]);
|
|
206
206
|
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (storageConfigured === false && !storageChecked) {
|
|
209
|
+
setStorageChecked(true);
|
|
210
|
+
setShowStorageConfigModal(true);
|
|
211
|
+
}
|
|
212
|
+
}, [storageConfigured, storageChecked]);
|
|
213
|
+
|
|
207
214
|
useEffect(() => {
|
|
208
215
|
loadMedia();
|
|
209
216
|
}, [loadMedia]);
|
|
@@ -569,8 +576,8 @@ export function MediaGallery({
|
|
|
569
576
|
<div className="flex-1 flex flex-col min-h-0 bg-[var(--kyro-bg)]">
|
|
570
577
|
<div className="flex-1 overflow-y-auto py-8 px-4 custom-scrollbar">
|
|
571
578
|
{loading ? (
|
|
572
|
-
<div className="
|
|
573
|
-
<
|
|
579
|
+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
580
|
+
<Shimmer variant="media-card" count={12} />
|
|
574
581
|
</div>
|
|
575
582
|
) : items.length === 0 ? (
|
|
576
583
|
<div className="flex flex-col items-center justify-center py-32 text-center">
|
|
@@ -847,7 +854,7 @@ export function MediaGallery({
|
|
|
847
854
|
{/* Selection Footer */}
|
|
848
855
|
{selectedIds.size > 0 && (
|
|
849
856
|
<div className="fixed bottom-12 left-1/2 -translate-x-1/2 z-[60] bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-full shadow-2xl px-2 py-2 flex items-center gap-12 animate-in slide-in-from-bottom-12 duration-700 ring-1 ring-white/10 backdrop-blur-xl">
|
|
850
|
-
<div className="flex items-center gap-5 border-r border-[var(--kyro-border)]
|
|
857
|
+
<div className="flex items-center gap-5 border-r border-[var(--kyro-border)] ">
|
|
851
858
|
<div className="w-12 h-12 rounded-full bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] flex items-center justify-center text-lg font-bold shadow-inner">
|
|
852
859
|
{selectedIds.size}
|
|
853
860
|
</div>
|
|
@@ -30,7 +30,7 @@ interface NavItem {
|
|
|
30
30
|
icon: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const siteSettings = await getSiteSettings();
|
|
33
|
+
const siteSettings = await getSiteSettings({ request: Astro.request });
|
|
34
34
|
const siteName = siteSettings?.siteName || "KYRO.";
|
|
35
35
|
const siteLogo = siteSettings?.siteLogo;
|
|
36
36
|
const logoWidth = siteSettings?.logo?.width;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface ShimmerProps {
|
|
2
|
+
variant: "text" | "circle" | "rect" | "card" | "table-row" | "media-card" | "stat-card";
|
|
3
|
+
count?: number;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Shimmer({ variant, count = 1, className = "" }: ShimmerProps) {
|
|
8
|
+
const variants = {
|
|
9
|
+
text: "h-3 rounded-md",
|
|
10
|
+
circle: "size-10 rounded-full",
|
|
11
|
+
rect: "h-10 rounded-xl",
|
|
12
|
+
card: "h-32 rounded-2xl",
|
|
13
|
+
"table-row": "h-14 rounded-xl",
|
|
14
|
+
"media-card": "aspect-square rounded-2xl",
|
|
15
|
+
"stat-card": "h-24 rounded-2xl",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{Array.from({ length: count }).map((_, i) => (
|
|
21
|
+
<div
|
|
22
|
+
key={i}
|
|
23
|
+
className={`kyro-shimmer ${variants[variant]} ${className}`}
|
|
24
|
+
/>
|
|
25
|
+
))}
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -136,7 +136,7 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
|
136
136
|
};
|
|
137
137
|
|
|
138
138
|
return (
|
|
139
|
-
<div className="flex-1 overflow-y-auto
|
|
139
|
+
<div className="flex-1 overflow-y-auto space-y-8">
|
|
140
140
|
<div className="surface-tile p-6 flex items-center justify-between">
|
|
141
141
|
<div className="flex items-center gap-4">
|
|
142
142
|
<div className="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">
|
|
@@ -85,7 +85,7 @@ export function UserForm({ mode, apiPath, adminPath, user }: UserFormProps) {
|
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
return (
|
|
88
|
-
<div className="flex-1 overflow-y-auto
|
|
88
|
+
<div className="flex-1 overflow-y-auto space-y-8">
|
|
89
89
|
<div className="surface-tile p-6 flex items-center justify-between">
|
|
90
90
|
<div>
|
|
91
91
|
<h1 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
|
|
@@ -81,7 +81,7 @@ export function UsersList({
|
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
return (
|
|
84
|
-
<div className="flex-1 overflow-y-auto
|
|
84
|
+
<div className="flex-1 overflow-y-auto space-y-8">
|
|
85
85
|
<div className="surface-tile p-6 flex items-center justify-between">
|
|
86
86
|
<div>
|
|
87
87
|
<h1 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
|
|
@@ -389,11 +389,27 @@ const drafts = [browserDraft, serverDraft].filter(Boolean) as Array<{
|
|
|
389
389
|
setFormData,
|
|
390
390
|
]);
|
|
391
391
|
|
|
392
|
+
// Recursively find a field by name inside tabs/group/collapsible
|
|
393
|
+
function findFieldDeep(fields: Record<string, any>[], name: string): Record<string, any> | undefined {
|
|
394
|
+
for (const f of fields) {
|
|
395
|
+
if (f.name === name && f.admin?.autoGenerate === "title") return f;
|
|
396
|
+
if (f.type === "tabs" && "tabs" in f) {
|
|
397
|
+
for (const tab of f.tabs) {
|
|
398
|
+
const found = findFieldDeep(tab.fields, name);
|
|
399
|
+
if (found) return found;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if ((f.type === "group" || f.type === "collapsible") && "fields" in f) {
|
|
403
|
+
const found = findFieldDeep(f.fields, name);
|
|
404
|
+
if (found) return found;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
392
410
|
// Auto-generate metaTitle
|
|
393
411
|
useEffect(() => {
|
|
394
|
-
const metaTitleField = config.fields
|
|
395
|
-
(f: Record<string, unknown>) => f.name === "metaTitle" && f.admin?.autoGenerate === "title",
|
|
396
|
-
);
|
|
412
|
+
const metaTitleField = findFieldDeep(config.fields, "metaTitle");
|
|
397
413
|
if (!metaTitleField) return;
|
|
398
414
|
|
|
399
415
|
let titleValue = "";
|
package/src/integration.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
import { config as loadDotEnv } from "dotenv";
|
|
7
|
+
import { transform } from "esbuild";
|
|
4
8
|
|
|
5
9
|
export interface KyroAdminOptions {
|
|
6
10
|
basePath?: string;
|
|
@@ -18,7 +22,7 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
|
|
|
18
22
|
return {
|
|
19
23
|
name: "@kyro-cms/admin",
|
|
20
24
|
hooks: {
|
|
21
|
-
"astro:config:setup": ({ config, updateConfig, injectRoute, logger }) => {
|
|
25
|
+
"astro:config:setup": async ({ config, updateConfig, injectRoute, logger }) => {
|
|
22
26
|
logger.info(`Kyro Admin mounted at ${basePath} (API: ${apiPath})`);
|
|
23
27
|
|
|
24
28
|
const fallbackConfig = path.resolve(
|
|
@@ -42,19 +46,81 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
|
|
|
42
46
|
logger.warn(`Config file not found. Using defaults.`);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
// Load the user's config and expose it globally so that
|
|
50
|
+
// admin lib modules (config.ts, globals.ts) can access it
|
|
51
|
+
// without needing the kyro:config Vite alias during config loading.
|
|
52
|
+
// Load .env first since Vite hasn't processed it yet at this point
|
|
53
|
+
// in the lifecycle, and the config module evaluates eagerly.
|
|
54
|
+
// Use esbuild to transpile TS to ESM, then evaluate in a child
|
|
55
|
+
// process to completely bypass Vite's module runner interception.
|
|
56
|
+
let tmpFile = "";
|
|
57
|
+
try {
|
|
58
|
+
const envPath = path.join(path.dirname(resolvedConfig), ".env");
|
|
59
|
+
if (fs.existsSync(envPath)) {
|
|
60
|
+
loadDotEnv({ path: envPath });
|
|
61
|
+
}
|
|
62
|
+
const configContent = fs.readFileSync(resolvedConfig, "utf8");
|
|
63
|
+
const result = await transform(configContent, {
|
|
64
|
+
loader: "ts",
|
|
65
|
+
format: "esm",
|
|
66
|
+
target: "es2022",
|
|
67
|
+
sourcemap: false,
|
|
55
68
|
});
|
|
69
|
+
// Write transpiled config alongside original so Node.js can
|
|
70
|
+
// resolve @kyro-cms/core from the project's node_modules
|
|
71
|
+
tmpFile = resolvedConfig.replace(/\.ts$/, ".admin.mjs");
|
|
72
|
+
fs.writeFileSync(tmpFile, result.code, "utf8");
|
|
73
|
+
// Evaluate in a child process to bypass Vite's module runner.
|
|
74
|
+
// Write a wrapper entrypoint that imports the config and prints JSON,
|
|
75
|
+
// then execute it with tsx (handles .ts resolution from .js imports).
|
|
76
|
+
const entryFile = tmpFile.replace(/\.admin\.mjs$/, ".admin-entry.mjs");
|
|
77
|
+
const resultFile = tmpFile.replace(/\.admin\.mjs$/, ".admin-result.json");
|
|
78
|
+
fs.writeFileSync(entryFile, `
|
|
79
|
+
import cfg from './${path.basename(tmpFile)}';
|
|
80
|
+
import fs from 'fs';
|
|
81
|
+
const data = { collections: cfg.default?.collections || cfg?.collections || [], globals: cfg.default?.globals || cfg?.globals || [] };
|
|
82
|
+
fs.writeFileSync('${path.basename(resultFile)}', JSON.stringify(data));
|
|
83
|
+
`, "utf8");
|
|
84
|
+
execSync(
|
|
85
|
+
`npx tsx "${entryFile}"`,
|
|
86
|
+
{ cwd: path.dirname(resolvedConfig), encoding: "utf8", timeout: 15000, stdio: "pipe" },
|
|
87
|
+
);
|
|
88
|
+
const resultContent = fs.readFileSync(resultFile, "utf8");
|
|
89
|
+
const configModule = JSON.parse(resultContent);
|
|
90
|
+
try { fs.unlinkSync(resultFile); } catch {}
|
|
91
|
+
if (configModule.error) {
|
|
92
|
+
throw new Error(configModule.error);
|
|
93
|
+
}
|
|
94
|
+
(globalThis as any).__KYRO_ADMIN_PROJECT_CONFIG__ = {
|
|
95
|
+
collections: configModule.collections,
|
|
96
|
+
globals: configModule.globals,
|
|
97
|
+
adapter: configModule.adapter || null,
|
|
98
|
+
};
|
|
99
|
+
logger.info("Project config loaded for admin");
|
|
100
|
+
} catch (e: any) {
|
|
101
|
+
logger.warn(`Could not load project config: ${e.message}`);
|
|
102
|
+
} finally {
|
|
103
|
+
for (const suffix of [".admin.mjs", ".admin-entry.mjs", ".admin-result.json"]) {
|
|
104
|
+
const f = resolvedConfig.replace(/\.ts$/, suffix);
|
|
105
|
+
if (fs.existsSync(f)) { try { fs.unlinkSync(f); } catch { /* ignore */ } }
|
|
106
|
+
}
|
|
56
107
|
}
|
|
57
108
|
|
|
109
|
+
// Set up Vite aliases and defines for runtime use
|
|
110
|
+
updateConfig({
|
|
111
|
+
vite: {
|
|
112
|
+
resolve: {
|
|
113
|
+
alias: {
|
|
114
|
+
"kyro:config": resolvedConfig,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
define: {
|
|
118
|
+
__KYRO_ADMIN_PATH__: JSON.stringify(basePath),
|
|
119
|
+
__KYRO_API_PATH__: JSON.stringify(apiPath),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
58
124
|
// Inject Admin UI Routes
|
|
59
125
|
const pages = [
|
|
60
126
|
{ pattern: "", entrypoint: "./pages/index.astro" },
|
|
@@ -104,20 +170,6 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
|
|
|
104
170
|
),
|
|
105
171
|
});
|
|
106
172
|
}
|
|
107
|
-
|
|
108
|
-
updateConfig({
|
|
109
|
-
vite: {
|
|
110
|
-
resolve: {
|
|
111
|
-
alias: {
|
|
112
|
-
"kyro:config": resolvedConfig,
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
define: {
|
|
116
|
-
__KYRO_ADMIN_PATH__: JSON.stringify(basePath),
|
|
117
|
-
__KYRO_API_PATH__: JSON.stringify(apiPath),
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
173
|
},
|
|
122
174
|
"astro:build:done": ({ logger }) => {
|
|
123
175
|
logger.info("Kyro Admin build complete");
|
|
@@ -14,7 +14,7 @@ interface Props {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const { title } = Astro.props;
|
|
17
|
-
const siteSettings = await getSiteSettings();
|
|
17
|
+
const siteSettings = await getSiteSettings({ request: Astro.request });
|
|
18
18
|
const siteName = siteSettings?.siteName || "Kyro CMS";
|
|
19
19
|
const siteFavicon = siteSettings?.siteFavicon;
|
|
20
20
|
---
|
|
@@ -25,38 +25,42 @@ const siteFavicon = siteSettings?.siteFavicon;
|
|
|
25
25
|
<meta charset="UTF-8" />
|
|
26
26
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
27
27
|
<title>{title} - {siteName}</title>
|
|
28
|
-
<link rel="icon" type={siteFavicon?.mimeType || "image/svg+xml"} href={siteFavicon?.url || "/favicon.svg"} />
|
|
29
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
30
28
|
<link
|
|
31
|
-
rel="
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
rel="icon"
|
|
30
|
+
type={siteFavicon?.mimeType || "image/svg+xml"}
|
|
31
|
+
href={siteFavicon?.url || "/favicon.svg"}
|
|
34
32
|
/>
|
|
33
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
34
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
35
35
|
<link
|
|
36
36
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap"
|
|
37
37
|
rel="stylesheet"
|
|
38
38
|
/>
|
|
39
39
|
<script is:inline define:vars={{ adminPath, apiPath }}>
|
|
40
40
|
// Simple in-memory auth state (alternative to Zustand for SSR)
|
|
41
|
-
window.__kyroAuth = window.__kyroAuth || {
|
|
41
|
+
window.__kyroAuth = window.__kyroAuth || {
|
|
42
|
+
user: null,
|
|
43
|
+
permissions: null,
|
|
44
|
+
verified: false,
|
|
45
|
+
};
|
|
42
46
|
|
|
43
47
|
// Verify auth - redirect to login if not authenticated
|
|
44
48
|
(async () => {
|
|
45
49
|
try {
|
|
46
50
|
// Fetch user and permissions using cookies
|
|
47
51
|
const [meRes, accessRes] = await Promise.all([
|
|
48
|
-
fetch(apiPath +
|
|
49
|
-
fetch(apiPath +
|
|
52
|
+
fetch(apiPath + "/auth/me", { credentials: "include" }),
|
|
53
|
+
fetch(apiPath + "/auth/access", { credentials: "include" }),
|
|
50
54
|
]);
|
|
51
55
|
|
|
52
56
|
if (!meRes.ok) {
|
|
53
57
|
window.location.href = adminPath + "/login";
|
|
54
58
|
return;
|
|
55
59
|
}
|
|
56
|
-
|
|
60
|
+
|
|
57
61
|
const meData = await meRes.json();
|
|
58
62
|
if (!meData.user) {
|
|
59
|
-
console.log(
|
|
63
|
+
console.log("[AdminLayout] No user in data, redirecting to login");
|
|
60
64
|
window.location.href = adminPath + "/login";
|
|
61
65
|
return;
|
|
62
66
|
}
|
|
@@ -67,52 +71,70 @@ const siteFavicon = siteSettings?.siteFavicon;
|
|
|
67
71
|
window.__kyroAuth.user = meData.user;
|
|
68
72
|
window.__kyroAuth.permissions = permissions;
|
|
69
73
|
window.__kyroAuth.verified = true;
|
|
70
|
-
|
|
74
|
+
|
|
71
75
|
// Dispatch event for components to update
|
|
72
|
-
window.dispatchEvent(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
window.dispatchEvent(
|
|
77
|
+
new CustomEvent("kyro:auth-ready", {
|
|
78
|
+
detail: {
|
|
79
|
+
user: meData.user,
|
|
80
|
+
permissions,
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
78
84
|
|
|
79
85
|
// Navigation Guard - Check if user has read access to current page
|
|
80
86
|
if (permissions) {
|
|
81
87
|
const path = window.location.pathname;
|
|
82
|
-
const relativePath = path.replace(adminPath,
|
|
83
|
-
|
|
88
|
+
const relativePath = path.replace(adminPath, "");
|
|
89
|
+
|
|
84
90
|
// Extract slug and type
|
|
85
|
-
let slug =
|
|
86
|
-
let type =
|
|
87
|
-
|
|
88
|
-
if (relativePath.startsWith(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
else if (relativePath.startsWith(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
let slug = "";
|
|
92
|
+
let type = "";
|
|
93
|
+
|
|
94
|
+
if (relativePath.startsWith("/users")) {
|
|
95
|
+
slug = "users";
|
|
96
|
+
type = "collection";
|
|
97
|
+
} else if (relativePath.startsWith("/audit")) {
|
|
98
|
+
slug = "audit_logs";
|
|
99
|
+
type = "collection";
|
|
100
|
+
} else if (relativePath.startsWith("/media")) {
|
|
101
|
+
slug = "media";
|
|
102
|
+
type = "collection";
|
|
103
|
+
} else if (relativePath.startsWith("/settings/")) {
|
|
104
|
+
slug = relativePath.split("/")[2];
|
|
105
|
+
type = "global";
|
|
106
|
+
} else if (
|
|
107
|
+
relativePath.includes("/") &&
|
|
108
|
+
!relativePath.startsWith("/login") &&
|
|
109
|
+
!relativePath.startsWith("/403")
|
|
110
|
+
) {
|
|
111
|
+
// Dynamic collections: /[adminPath]/[collectionSlug]/...
|
|
112
|
+
slug = relativePath.split("/")[1];
|
|
113
|
+
type = "collection";
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
if (slug && type) {
|
|
99
117
|
let hasAccess = true;
|
|
100
|
-
if (type ===
|
|
118
|
+
if (type === "collection" && permissions.collections) {
|
|
101
119
|
const p = permissions.collections[slug];
|
|
102
120
|
if (p && p.read === false) hasAccess = false;
|
|
103
|
-
} else if (type ===
|
|
121
|
+
} else if (type === "global" && permissions.globals) {
|
|
104
122
|
const p = permissions.globals[slug];
|
|
105
123
|
if (p && p.read === false) hasAccess = false;
|
|
106
124
|
}
|
|
107
|
-
|
|
125
|
+
|
|
108
126
|
if (!hasAccess) {
|
|
109
|
-
console.log(
|
|
127
|
+
console.log(
|
|
128
|
+
"[AdminLayout] Access denied for",
|
|
129
|
+
slug,
|
|
130
|
+
"redirecting to 403",
|
|
131
|
+
);
|
|
110
132
|
window.location.href = adminPath + "/403";
|
|
111
133
|
}
|
|
112
134
|
}
|
|
113
135
|
}
|
|
114
136
|
} catch (err) {
|
|
115
|
-
console.error(
|
|
137
|
+
console.error("[AdminLayout] Auth check error:", err);
|
|
116
138
|
window.location.href = adminPath + "/login";
|
|
117
139
|
}
|
|
118
140
|
})();
|
|
@@ -291,18 +313,18 @@ const siteFavicon = siteSettings?.siteFavicon;
|
|
|
291
313
|
|
|
292
314
|
logoutBackdrop?.addEventListener("click", closeLogoutModal);
|
|
293
315
|
logoutCancel?.addEventListener("click", closeLogoutModal);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
316
|
+
logoutConfirm?.addEventListener("click", async () => {
|
|
317
|
+
try {
|
|
318
|
+
await fetch(apiPath + "/auth/logout", {
|
|
319
|
+
method: "POST",
|
|
320
|
+
credentials: "include",
|
|
321
|
+
});
|
|
322
|
+
} finally {
|
|
323
|
+
// Clear auth state
|
|
324
|
+
window.__kyroAuth = { user: null, verified: false };
|
|
325
|
+
window.location.href = adminPath + "/login";
|
|
326
|
+
}
|
|
327
|
+
});
|
|
306
328
|
</script>
|
|
307
329
|
</body>
|
|
308
330
|
</html>
|
package/src/lib/config.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
|
|
2
|
-
import projectConfig from "kyro:config";
|
|
3
2
|
import {
|
|
4
3
|
blogCollections,
|
|
5
4
|
ecommerceCollections,
|
|
@@ -122,6 +121,12 @@ function createProjectAdminConfig(config: {
|
|
|
122
121
|
};
|
|
123
122
|
}
|
|
124
123
|
|
|
124
|
+
// Default to kitchen-sink during module evaluation (config loading phase).
|
|
125
|
+
// The real config is set later by the admin integration via globalThis.
|
|
126
|
+
const global = globalThis as any;
|
|
127
|
+
const projectConfig: { collections?: ConfigCollectionInput; globals?: ConfigGlobalInput } =
|
|
128
|
+
global.__KYRO_ADMIN_PROJECT_CONFIG__ || {};
|
|
129
|
+
|
|
125
130
|
export const adminConfig = createProjectAdminConfig(projectConfig);
|
|
126
131
|
export const collections = adminConfig.collections;
|
|
127
132
|
export const globals = adminConfig.globals;
|