@kyro-cms/admin 0.3.1 → 0.3.4
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-XEUOVAAC.js +466 -0
- package/dist/EditorClient-XEUOVAAC.js.map +1 -0
- package/dist/EditorClient-YLCGVDXY.cjs +468 -0
- package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
- package/dist/chunk-7KPIUCGT.js +384 -0
- package/dist/chunk-7KPIUCGT.js.map +1 -0
- package/dist/chunk-GOACG6R7.cjs +473 -0
- package/dist/chunk-GOACG6R7.cjs.map +1 -0
- package/dist/index.cjs +14861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +1661 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +563 -0
- package/dist/index.js +14784 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -19
- package/src/components/ActionBar.tsx +7 -43
- package/src/components/Admin.tsx +138 -277
- package/src/components/ApiKeysManager.tsx +428 -419
- package/src/components/AuditLogsPage.tsx +35 -39
- package/src/components/AuthBridge.tsx +51 -0
- package/src/components/AutoForm.tsx +495 -1230
- package/src/components/BrandingHub.tsx +18 -19
- package/src/components/BulkActionsBar.tsx +1 -1
- package/src/components/CreateView.tsx +22 -36
- package/src/components/Dashboard.tsx +60 -84
- package/src/components/DetailView.tsx +113 -91
- package/src/components/DeveloperCenter.tsx +200 -198
- package/src/components/FieldRenderer.tsx +206 -0
- package/src/components/GraphQLPlayground.tsx +340 -480
- package/src/components/ListView.tsx +828 -254
- package/src/components/LoginPage.tsx +3 -4
- package/src/components/MarketplaceManager.tsx +254 -0
- package/src/components/MediaGallery.tsx +856 -1192
- package/src/components/PluginsManager.tsx +277 -0
- package/src/components/RestPlayground.tsx +398 -560
- package/src/components/SessionsManager.tsx +211 -0
- package/src/components/Sidebar.astro +179 -151
- package/src/components/ThemeProvider.tsx +7 -161
- package/src/components/UserManagement.tsx +162 -146
- package/src/components/UserMenu.tsx +110 -0
- package/src/components/WebhookManager.tsx +305 -367
- package/src/components/blocks/AccordionBlock.tsx +4 -4
- package/src/components/blocks/ArrayBlock.tsx +3 -3
- package/src/components/blocks/BlockEditModal.tsx +8 -8
- package/src/components/blocks/BlockWrapper.tsx +61 -0
- package/src/components/blocks/ButtonBlock.tsx +4 -4
- package/src/components/blocks/ChildBlocksTree.tsx +23 -25
- package/src/components/blocks/CodeBlock.tsx +15 -15
- package/src/components/blocks/ColumnsBlock.tsx +6 -44
- package/src/components/blocks/DividerBlock.tsx +3 -3
- package/src/components/blocks/FileBlock.tsx +4 -4
- package/src/components/blocks/HeadingBlock.tsx +6 -38
- package/src/components/blocks/HeroBlock.tsx +4 -4
- package/src/components/blocks/ImageBlock.tsx +4 -4
- package/src/components/blocks/LinkBlock.tsx +4 -4
- package/src/components/blocks/ListBlock.tsx +3 -3
- package/src/components/blocks/ParagraphBlock.tsx +12 -42
- package/src/components/blocks/RelationshipBlock.tsx +4 -4
- package/src/components/blocks/RichTextBlock.tsx +4 -4
- package/src/components/blocks/VStackBlock.tsx +5 -37
- package/src/components/blocks/VideoBlock.tsx +4 -4
- package/src/components/blocks/types.ts +11 -0
- package/src/components/fields/AccordionField.tsx +1 -1
- package/src/components/fields/ArrayField.tsx +2 -2
- package/src/components/fields/ArrayLayout.tsx +93 -0
- package/src/components/fields/BlocksField.tsx +122 -111
- package/src/components/fields/ButtonField.tsx +1 -1
- package/src/components/fields/CheckboxField.tsx +14 -15
- package/src/components/fields/ChildrenField.tsx +2 -2
- package/src/components/fields/CodeField.tsx +3 -3
- package/src/components/fields/ColumnsField.tsx +2 -2
- package/src/components/fields/DateField.tsx +13 -26
- package/src/components/fields/EditorClient.tsx +26 -28
- package/src/components/fields/FieldLayout.tsx +52 -0
- package/src/components/fields/GroupLayout.tsx +35 -0
- package/src/components/fields/JSONField.tsx +7 -7
- package/src/components/fields/LinkField.tsx +1 -1
- package/src/components/fields/MarkdownField.tsx +1 -1
- package/src/components/fields/NumberField.tsx +13 -26
- package/src/components/fields/PortableTextField.tsx +4 -4
- package/src/components/fields/PortableTextRenderer.tsx +1 -1
- package/src/components/fields/RelationshipBlockField.tsx +31 -23
- package/src/components/fields/RelationshipField.tsx +14 -14
- package/src/components/fields/SelectField.tsx +17 -26
- package/src/components/fields/TabsLayout.tsx +69 -0
- package/src/components/fields/TextField.tsx +85 -38
- package/src/components/fields/UploadField.tsx +71 -41
- package/src/components/fields/VideoField.tsx +1 -1
- package/src/components/fields/extensions/blockComponents.tsx +2 -2
- package/src/components/fields/extensions/blocksStore.ts +207 -193
- package/src/components/fields/types.ts +22 -0
- package/src/components/layout/Layout.tsx +1 -1
- package/src/components/ui/ActionMenu.tsx +63 -0
- package/src/components/ui/Badge.tsx +59 -5
- package/src/components/ui/BlockDrawer.tsx +4 -5
- package/src/components/ui/CommandPalette.tsx +58 -36
- package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
- package/src/components/ui/Dropdown.tsx +18 -16
- package/src/components/ui/EmptyState.tsx +25 -0
- package/src/components/ui/GlobalModal.tsx +49 -0
- package/src/components/ui/IconButton.tsx +44 -0
- package/src/components/ui/Modal.tsx +19 -20
- package/src/components/ui/PageHeader.tsx +158 -0
- package/src/components/ui/Pagination.tsx +61 -0
- package/src/components/ui/PromptModal.tsx +1 -1
- package/src/components/ui/SearchInput.tsx +57 -0
- package/src/components/ui/SeoPreview.tsx +31 -0
- package/src/components/ui/SessionModal.tsx +0 -0
- package/src/components/ui/SlidePanel.tsx +2 -0
- package/src/components/ui/Toast.tsx +65 -122
- package/src/components/ui/Toaster.tsx +18 -0
- package/src/components/ui/icons.tsx +112 -0
- package/src/components/users/UserDetail.tsx +290 -0
- package/src/components/users/UserForm.tsx +242 -0
- package/src/components/users/UsersList.tsx +338 -0
- package/src/env.d.ts +13 -13
- package/src/fields/index.ts +2 -1
- package/src/global.d.ts +7 -0
- package/src/hooks/data.ts +2 -9
- package/src/hooks/useAsyncData.ts +36 -0
- package/src/hooks/useAutoFormState.ts +527 -0
- package/src/hooks/useSelection.ts +49 -0
- package/src/hooks/useSession.ts +0 -0
- package/src/index.ts +11 -1
- package/src/integration.ts +86 -11
- package/src/kyro-cms.d.ts +209 -0
- package/src/layouts/AdminLayout.astro +128 -11
- package/src/layouts/AuthLayout.astro +21 -5
- package/src/lib/api.ts +175 -55
- package/src/lib/autoform-store.ts +435 -0
- package/src/lib/config.ts +82 -34
- package/src/lib/createRegistry.ts +29 -0
- package/src/lib/default-kyro-config.ts +4 -0
- package/src/lib/globals.ts +50 -0
- package/src/lib/media-utils.ts +18 -0
- package/src/lib/object-utils.ts +77 -0
- package/src/lib/paths.ts +61 -0
- package/src/lib/stores/index.ts +370 -0
- package/src/lib/types.ts +43 -0
- package/src/lib/useResourceManager.ts +105 -0
- package/src/pages/403.astro +67 -0
- package/src/pages/[collection]/[id].astro +14 -180
- package/src/pages/[collection]/index.astro +11 -6
- package/src/pages/api-explorer.astro +173 -0
- package/src/pages/audit/index.astro +2 -0
- package/src/pages/auth/login.astro +122 -0
- package/src/pages/auth/register.astro +167 -0
- package/src/pages/graphql-explorer.astro +59 -0
- package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
- package/src/pages/index.astro +577 -0
- package/src/pages/index_ALT.astro +3 -0
- package/src/pages/keys.astro +11 -0
- package/src/pages/marketplace.astro +11 -0
- package/src/pages/media.astro +3 -0
- package/src/pages/plugins.astro +8 -0
- package/src/pages/preview/[collection]/[id].astro +188 -123
- package/src/pages/rest-playground.astro +62 -0
- package/src/pages/roles/index.astro +183 -76
- package/src/pages/sessions.astro +8 -0
- package/src/pages/settings/[slug].astro +92 -114
- package/src/pages/settings/index.astro +5 -3
- package/src/pages/users/[id].astro +25 -154
- package/src/pages/users/index.astro +19 -130
- package/src/pages/users/new.astro +9 -86
- package/src/pages/webhooks.astro +11 -0
- package/src/routes.ts +80 -0
- package/src/styles/main.css +119 -79
- package/src/theme/tokens.ts +1 -0
- package/src/vite-env.d.ts +14 -0
- package/src/collections/auth/index.ts +0 -155
- package/src/collections/portfolio/index.ts +0 -343
- package/src/components/ApiExplorer.tsx +0 -325
- package/src/components/EnhancedListView.tsx +0 -889
- package/src/components/GraphQLExplorer.tsx +0 -675
- package/src/components/Icons.tsx +0 -23
- package/src/components/StatusBadge.tsx +0 -76
- package/src/lib/MediaService.ts +0 -541
- package/src/lib/auth/sqlite-adapter.ts +0 -319
- package/src/lib/dataStore.ts +0 -226
- package/src/lib/db/adapter.ts +0 -54
- package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
- package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
- package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
- package/src/lib/db/index.ts +0 -449
- package/src/lib/db/mongodb-adapter.ts +0 -207
- package/src/lib/db/mongodb-auth-adapter.ts +0 -305
- package/src/lib/db/schema/mysql-auth.ts +0 -113
- package/src/lib/db/schema/mysql-content.ts +0 -20
- package/src/lib/db/schema/postgres-auth.ts +0 -116
- package/src/lib/db/schema/postgres-content.ts +0 -35
- package/src/lib/db/schema/postgres-media.ts +0 -52
- package/src/lib/db/schema/postgres-settings.ts +0 -11
- package/src/lib/db/schema/sqlite-auth.ts +0 -112
- package/src/lib/db/schema/sqlite-content.ts +0 -20
- package/src/lib/db/version-adapter.ts +0 -248
- package/src/lib/graphql/index.ts +0 -1
- package/src/lib/graphql/schema.ts +0 -443
- package/src/lib/rate-limit.ts +0 -267
- package/src/lib/storage.ts +0 -374
- package/src/lib/store.ts +0 -85
- package/src/middleware.ts +0 -177
- package/src/pages/admin/api-explorer.astro +0 -98
- package/src/pages/admin/graphql-explorer.astro +0 -40
- package/src/pages/admin/index.astro +0 -286
- package/src/pages/admin/keys.astro +0 -8
- package/src/pages/admin/rest-playground.astro +0 -44
- package/src/pages/admin/webhooks.astro +0 -8
- package/src/pages/api/[collection]/[id]/publish.ts +0 -52
- package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
- package/src/pages/api/[collection]/[id]/versions.ts +0 -66
- package/src/pages/api/[collection]/[id].ts +0 -213
- package/src/pages/api/[collection]/index.ts +0 -209
- package/src/pages/api/auth/[id].ts +0 -121
- package/src/pages/api/auth/audit-logs.ts +0 -57
- package/src/pages/api/auth/login.ts +0 -211
- package/src/pages/api/auth/logout.ts +0 -66
- package/src/pages/api/auth/me.ts +0 -36
- package/src/pages/api/auth/refresh.ts +0 -119
- package/src/pages/api/auth/register.ts +0 -188
- package/src/pages/api/auth/users.ts +0 -97
- package/src/pages/api/collections.ts +0 -59
- package/src/pages/api/globals/[slug].ts +0 -42
- package/src/pages/api/graphql.ts +0 -90
- package/src/pages/api/health.ts +0 -426
- package/src/pages/api/keys/[id].ts +0 -26
- package/src/pages/api/keys/index.ts +0 -75
- package/src/pages/api/media/[id].ts +0 -309
- package/src/pages/api/media/folders.ts +0 -609
- package/src/pages/api/media/index.ts +0 -146
- package/src/pages/api/media/resize.ts +0 -267
- package/src/pages/api/search.ts +0 -82
- package/src/pages/api/slug-availability.ts +0 -70
- package/src/pages/api/storage-config.ts +0 -20
- package/src/pages/api/storage-status.ts +0 -206
- package/src/pages/api/upload.ts +0 -334
- package/src/pages/api/webhooks/index.ts +0 -71
- package/src/pages/login.astro +0 -82
- package/src/pages/register.astro +0 -102
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { apiGet, apiDelete } from "../lib/api";
|
|
3
|
+
import { Shield, Monitor, Trash2, Clock, AlertTriangle, Info, LogOut, Globe, Activity, RefreshCcw, Smartphone, Laptop } from "./ui/icons";
|
|
4
|
+
import { PageHeader } from "./ui/PageHeader";
|
|
5
|
+
import { Badge } from "./ui/Badge";
|
|
6
|
+
|
|
7
|
+
interface Session {
|
|
8
|
+
id: string;
|
|
9
|
+
sessionName: string;
|
|
10
|
+
currentSession: boolean;
|
|
11
|
+
deviceInfo?: {
|
|
12
|
+
userAgent?: string;
|
|
13
|
+
ip?: string;
|
|
14
|
+
platform?: string;
|
|
15
|
+
browser?: string;
|
|
16
|
+
device?: string;
|
|
17
|
+
};
|
|
18
|
+
createdAt: number;
|
|
19
|
+
lastActivityAt: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function timeAgo(ts: number): string {
|
|
23
|
+
const diff = Date.now() - ts;
|
|
24
|
+
const mins = Math.floor(diff / 60000);
|
|
25
|
+
const hrs = Math.floor(diff / 3600000);
|
|
26
|
+
const days = Math.floor(diff / 86400000);
|
|
27
|
+
if (mins < 1) return "Just now";
|
|
28
|
+
if (mins < 60) return `${mins}m ago`;
|
|
29
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
30
|
+
if (days < 7) return `${days}d ago`;
|
|
31
|
+
return new Date(ts).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function SessionsManager() {
|
|
35
|
+
const [sessions, setSessions] = useState<Session[]>([]);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState("");
|
|
38
|
+
const [revokingId, setRevokingId] = useState<string | null>(null);
|
|
39
|
+
const [revokingAll, setRevokingAll] = useState(false);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
apiGet<{ sessions: Session[] }>("/api/auth/sessions")
|
|
43
|
+
.then((r) => {
|
|
44
|
+
setSessions(Array.isArray(r.sessions) ? r.sessions : []);
|
|
45
|
+
setError("");
|
|
46
|
+
})
|
|
47
|
+
.catch(() => setError("Failed to load sessions"))
|
|
48
|
+
.finally(() => setLoading(false));
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const revoke = async (id: string) => {
|
|
52
|
+
setRevokingId(id);
|
|
53
|
+
try {
|
|
54
|
+
await apiDelete(`/api/auth/sessions/${id}`);
|
|
55
|
+
setSessions((p) => p.filter((s) => s.id !== id));
|
|
56
|
+
} catch {
|
|
57
|
+
setError("Failed to revoke session");
|
|
58
|
+
} finally {
|
|
59
|
+
setRevokingId(null);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const revokeAll = async () => {
|
|
64
|
+
setRevokingAll(true);
|
|
65
|
+
try {
|
|
66
|
+
await apiDelete("/api/auth/sessions");
|
|
67
|
+
setSessions((p) => p.filter((s) => s.currentSession));
|
|
68
|
+
} catch {
|
|
69
|
+
setError("Failed to revoke sessions");
|
|
70
|
+
} finally {
|
|
71
|
+
setRevokingAll(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const otherCount = sessions.filter((s) => !s.currentSession).length;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
|
|
79
|
+
<PageHeader
|
|
80
|
+
title="Active Sessions"
|
|
81
|
+
description="Monitor and manage your cryptographic access across all devices."
|
|
82
|
+
icon={Monitor}
|
|
83
|
+
actions={otherCount > 0 ? [
|
|
84
|
+
{
|
|
85
|
+
label: `Revoke ${otherCount} Sessions`,
|
|
86
|
+
onClick: () => { if (confirm(`Revoke all ${otherCount} other sessions?`)) revokeAll(); },
|
|
87
|
+
icon: LogOut,
|
|
88
|
+
variant: "outline",
|
|
89
|
+
className: "text-red-500 hover:text-red-600 hover:bg-red-500/5 border-red-500/20"
|
|
90
|
+
}
|
|
91
|
+
] : undefined}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<div className="flex flex-col gap-8 surface-tile p-8">
|
|
95
|
+
{/* Security & Summary Row */}
|
|
96
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
97
|
+
<div className="lg:col-span-2 p-5 rounded-2xl bg-amber-500/5 border border-amber-500/10 flex items-center gap-4">
|
|
98
|
+
<div className="p-2.5 bg-amber-500/10 rounded-xl shrink-0">
|
|
99
|
+
<Shield className="w-4 h-4 text-amber-500" />
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<h4 className="text-[10px] font-bold uppercase tracking-widest text-amber-600 mb-0.5">Security Protocol</h4>
|
|
103
|
+
<p className="text-[11px] text-amber-900/60 leading-relaxed font-medium">
|
|
104
|
+
Active sessions authorize access to your identity. Revoke any unfamiliar devices immediately.
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="p-5 rounded-2xl bg-[var(--kyro-bg-secondary)]/50 border border-[var(--kyro-border)] flex items-center gap-4">
|
|
109
|
+
<div className="p-2.5 bg-[var(--kyro-primary)]/5 rounded-xl">
|
|
110
|
+
<Activity className="w-4 h-4 text-[var(--kyro-primary)]" />
|
|
111
|
+
</div>
|
|
112
|
+
<div>
|
|
113
|
+
<div className="text-[9px] font-bold opacity-40 uppercase tracking-widest mb-0.5">Total Load</div>
|
|
114
|
+
<div className="text-sm font-bold tracking-tight">{sessions.length} Active Nodes</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{error && (
|
|
120
|
+
<div className="p-4 bg-red-500/10 border border-red-500/20 flex items-center gap-4 rounded-xl animate-pulse">
|
|
121
|
+
<AlertTriangle className="w-4 h-4 text-red-500" />
|
|
122
|
+
<span className="text-[11px] font-bold text-red-600 uppercase tracking-widest">{error}</span>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
<section className="space-y-6">
|
|
127
|
+
<div className="flex items-center justify-between gap-4 pb-4 border-b border-[var(--kyro-border)]/50">
|
|
128
|
+
<div className="flex items-center gap-2 px-1">
|
|
129
|
+
<div className="w-0.5 h-3 bg-[var(--kyro-primary)] rounded-full" />
|
|
130
|
+
<h2 className="text-[10px] font-bold tracking-[0.2em] opacity-40 uppercase">Authenticated Hosts</h2>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{loading ? (
|
|
135
|
+
<div className="p-12 text-center rounded-3xl border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30">
|
|
136
|
+
<RefreshCcw className="w-8 h-8 mx-auto mb-3 text-[var(--kyro-primary)] animate-spin-slow opacity-40" />
|
|
137
|
+
<p className="text-[10px] font-bold uppercase tracking-widest opacity-30">Analyzing session matrix...</p>
|
|
138
|
+
</div>
|
|
139
|
+
) : sessions.length === 0 ? (
|
|
140
|
+
<div className="p-12 text-center rounded-3xl border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30">
|
|
141
|
+
<Monitor className="w-10 h-10 mx-auto mb-3 text-[var(--kyro-text-secondary)] opacity-20" />
|
|
142
|
+
<h3 className="text-lg font-bold mb-1">No active sessions</h3>
|
|
143
|
+
<p className="text-xs text-[var(--kyro-text-secondary)] opacity-50">Identity manifest is currently clear.</p>
|
|
144
|
+
</div>
|
|
145
|
+
) : (
|
|
146
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
147
|
+
{sessions.map((s) => (
|
|
148
|
+
<div
|
|
149
|
+
key={s.id}
|
|
150
|
+
className={`group relative overflow-hidden bg-[var(--kyro-bg-secondary)]/30 border border-[var(--kyro-border)] rounded-2xl p-5 hover:border-[var(--kyro-primary)]/50 transition-all duration-300 ${s.currentSession ? "ring-1 ring-[var(--kyro-primary)]/20 bg-[var(--kyro-primary)]/[0.02]" : ""}`}
|
|
151
|
+
>
|
|
152
|
+
<div className="flex items-start justify-between gap-4 relative z-10">
|
|
153
|
+
<div className="flex items-center gap-3">
|
|
154
|
+
<div className={`p-2.5 rounded-xl transition-colors shadow-sm ${s.currentSession ? "bg-[var(--kyro-primary)] text-white" : "bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] border border-[var(--kyro-border)]"}`}>
|
|
155
|
+
{s.deviceInfo?.platform?.toLowerCase().includes("android") || s.deviceInfo?.platform?.toLowerCase().includes("ios")
|
|
156
|
+
? <Smartphone className="w-4 h-4" />
|
|
157
|
+
: <Laptop className="w-4 h-4" />}
|
|
158
|
+
</div>
|
|
159
|
+
<div>
|
|
160
|
+
<div className="flex items-center gap-2 mb-1">
|
|
161
|
+
<h3 className="font-bold text-sm leading-none">
|
|
162
|
+
{s.sessionName || s.deviceInfo?.device || "Authorized Host"}
|
|
163
|
+
</h3>
|
|
164
|
+
{s.currentSession && (
|
|
165
|
+
<Badge variant="success" className="text-[7px] font-bold uppercase tracking-tighter px-1 py-0 animate-pulse">Live</Badge>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
<div className="flex items-center gap-3 text-[9px] font-bold opacity-30 uppercase tracking-widest">
|
|
169
|
+
<span className="flex items-center gap-1">
|
|
170
|
+
<Globe className="w-2.5 h-2.5" />
|
|
171
|
+
{s.deviceInfo?.ip || "Unknown"}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{!s.currentSession && (
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={() => { if (confirm("Revoke this session?")) revoke(s.id); }}
|
|
181
|
+
disabled={revokingId === s.id}
|
|
182
|
+
className="p-2 bg-red-500/5 text-red-500/30 rounded-lg hover:bg-red-500/10 hover:text-red-500 transition-all border border-transparent hover:border-red-500/20"
|
|
183
|
+
>
|
|
184
|
+
{revokingId === s.id
|
|
185
|
+
? <RefreshCcw className="w-3.5 h-3.5 animate-spin" />
|
|
186
|
+
: <LogOut className="w-3.5 h-3.5" />}
|
|
187
|
+
</button>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div className="mt-4 pt-4 border-t border-[var(--kyro-border)]/50 grid grid-cols-2 gap-3">
|
|
192
|
+
<div className="space-y-0.5">
|
|
193
|
+
<span className="text-[8px] font-bold uppercase opacity-30 tracking-tighter">Environment</span>
|
|
194
|
+
<div className="text-[10px] font-bold truncate opacity-70">
|
|
195
|
+
{s.deviceInfo?.browser || "System Browser"}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="space-y-0.5">
|
|
199
|
+
<span className="text-[8px] font-bold uppercase opacity-30 tracking-tighter">Last Seen</span>
|
|
200
|
+
<div className="text-[10px] font-bold opacity-70">{timeAgo(s.lastActivityAt)}</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</section>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
import "../styles/main.css";
|
|
3
|
-
import { nonAuthCollections } from "
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import { nonAuthCollections } from "../lib/config";
|
|
4
|
+
import { adminPath } from "../lib/paths";
|
|
5
|
+
import { getSiteSettings } from "../lib/globals";
|
|
6
|
+
import {
|
|
7
|
+
Home,
|
|
8
|
+
Database,
|
|
9
|
+
Settings,
|
|
10
|
+
Users,
|
|
11
|
+
Shield,
|
|
12
|
+
FileText,
|
|
13
|
+
Clock,
|
|
14
|
+
Blocks,
|
|
15
|
+
Key,
|
|
16
|
+
Webhook,
|
|
17
|
+
Grid,
|
|
18
|
+
User,
|
|
19
|
+
LogOut,
|
|
20
|
+
Sun,
|
|
21
|
+
Moon,
|
|
22
|
+
Menu,
|
|
23
|
+
Dot,
|
|
24
|
+
} from "./ui/icons";
|
|
25
|
+
import { UserMenu } from "./UserMenu";
|
|
11
26
|
|
|
12
27
|
interface NavItem {
|
|
13
28
|
href: string;
|
|
@@ -15,24 +30,39 @@ interface NavItem {
|
|
|
15
30
|
icon: string;
|
|
16
31
|
}
|
|
17
32
|
|
|
33
|
+
const siteSettings = await getSiteSettings();
|
|
34
|
+
const siteName = siteSettings?.siteName || "KYRO.";
|
|
35
|
+
const siteLogo = siteSettings?.siteLogo;
|
|
36
|
+
const logoWidth = siteSettings?.logo?.width;
|
|
37
|
+
const logoHeight = siteSettings?.logo?.height;
|
|
38
|
+
const logoAlt = siteSettings?.logo?.altText || siteName;
|
|
39
|
+
|
|
18
40
|
interface Props {
|
|
19
41
|
title: string;
|
|
20
|
-
user?: User;
|
|
21
42
|
}
|
|
22
43
|
|
|
23
|
-
const { title
|
|
44
|
+
const { title } = Astro.props;
|
|
24
45
|
const currentPath = Astro.url.pathname;
|
|
25
46
|
|
|
26
|
-
const collectionItems: NavItem[] = nonAuthCollections
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
const collectionItems: NavItem[] = nonAuthCollections
|
|
48
|
+
.filter((col) => col.slug !== "media") //removed here cos it's been added manually in the navSections.
|
|
49
|
+
.map((col) => ({
|
|
50
|
+
href: `${adminPath}/${col.slug}`,
|
|
51
|
+
label: col.label || col.slug,
|
|
52
|
+
icon: "collection",
|
|
53
|
+
}));
|
|
31
54
|
|
|
32
55
|
const navSections: { label: string; items: NavItem[] }[] = [
|
|
33
56
|
{
|
|
34
57
|
label: "Home",
|
|
35
|
-
items: [
|
|
58
|
+
items: [
|
|
59
|
+
{ href: adminPath, label: "Dashboard", icon: "home" },
|
|
60
|
+
{
|
|
61
|
+
href: adminPath + "/media",
|
|
62
|
+
label: "Media Library",
|
|
63
|
+
icon: "media",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
36
66
|
},
|
|
37
67
|
{
|
|
38
68
|
label: "Collections",
|
|
@@ -41,51 +71,48 @@ const navSections: { label: string; items: NavItem[] }[] = [
|
|
|
41
71
|
{
|
|
42
72
|
label: "Settings",
|
|
43
73
|
items: [
|
|
44
|
-
{ href: "/
|
|
45
|
-
{ href: "/
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
label: "Developer",
|
|
52
|
-
items: [
|
|
53
|
-
{ href: "/admin/keys", label: "API Keys", icon: "keys" },
|
|
54
|
-
{ href: "/admin/webhooks", label: "Webhooks", icon: "webhooks" },
|
|
74
|
+
{ href: adminPath + "/users", label: "Users", icon: "users" },
|
|
75
|
+
{ href: adminPath + "/plugins", label: "Plugins", icon: "plugins" },
|
|
76
|
+
{
|
|
77
|
+
href: adminPath + "/settings",
|
|
78
|
+
label: "Settings",
|
|
79
|
+
icon: "settings",
|
|
80
|
+
},
|
|
55
81
|
],
|
|
56
82
|
},
|
|
57
83
|
];
|
|
58
84
|
|
|
59
|
-
const icons: Record<string,
|
|
60
|
-
home:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
moon: "M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z",
|
|
85
|
+
const icons: Record<string, any> = {
|
|
86
|
+
home: Home,
|
|
87
|
+
media: Grid,
|
|
88
|
+
collection: Dot,
|
|
89
|
+
menu: Menu,
|
|
90
|
+
settings: Settings,
|
|
91
|
+
users: Users,
|
|
92
|
+
shield: Shield,
|
|
93
|
+
audit: FileText,
|
|
94
|
+
plugins: Blocks,
|
|
95
|
+
keys: Key,
|
|
96
|
+
webhooks: Webhook,
|
|
97
|
+
marketplace: Grid,
|
|
98
|
+
user: User,
|
|
99
|
+
logout: LogOut,
|
|
100
|
+
sessions: Clock,
|
|
101
|
+
sun: Sun,
|
|
102
|
+
moon: Moon,
|
|
78
103
|
};
|
|
79
104
|
|
|
80
105
|
function isActive(item: NavItem): boolean {
|
|
81
|
-
if (item.href ===
|
|
82
|
-
if (item.href.startsWith("/settings"))
|
|
83
|
-
return currentPath.startsWith("/settings");
|
|
84
|
-
if (item.href === "/users") return title === "Users";
|
|
85
|
-
if (item.href === "/roles") return title === "
|
|
86
|
-
if (item.href === "/audit") return title === "Audit Logs";
|
|
87
|
-
if (item.href === "/
|
|
88
|
-
if (item.href === "/
|
|
106
|
+
if (item.href === adminPath) return title === "Dashboard";
|
|
107
|
+
if (item.href.startsWith(adminPath + "/settings"))
|
|
108
|
+
return currentPath.startsWith(adminPath + "/settings");
|
|
109
|
+
if (item.href === adminPath + "/users") return title === "Users";
|
|
110
|
+
if (item.href === adminPath + "/roles") return title === "Permissions";
|
|
111
|
+
if (item.href === adminPath + "/audit") return title === "Audit Logs";
|
|
112
|
+
if (item.href === adminPath + "/keys") return title === "API Keys";
|
|
113
|
+
if (item.href === adminPath + "/webhooks") return title === "Webhooks";
|
|
114
|
+
if (item.href === adminPath + "/plugins") return title === "Plugins";
|
|
115
|
+
if (item.href === adminPath + "/marketplace") return title === "Marketplace";
|
|
89
116
|
// Collections: match /collection-name and /collection-name/* paths
|
|
90
117
|
if (currentPath === item.href || currentPath.startsWith(item.href + "/"))
|
|
91
118
|
return true;
|
|
@@ -96,59 +123,116 @@ function isActive(item: NavItem): boolean {
|
|
|
96
123
|
<aside
|
|
97
124
|
class="surface-tile w-[320px] flex flex-col flex-shrink-0 overflow-hidden"
|
|
98
125
|
>
|
|
99
|
-
<div class="px-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
<div class="px-8 py-8 flex items-center gap-3">
|
|
127
|
+
{
|
|
128
|
+
siteLogo ? (
|
|
129
|
+
<img
|
|
130
|
+
src={siteLogo.url}
|
|
131
|
+
alt={logoAlt}
|
|
132
|
+
style={{
|
|
133
|
+
width: logoWidth ? `${logoWidth}px` : "auto",
|
|
134
|
+
height: logoHeight ? `${logoHeight}px` : "32px",
|
|
135
|
+
objectFit: "contain",
|
|
136
|
+
}}
|
|
137
|
+
class="rounded-lg"
|
|
138
|
+
/>
|
|
139
|
+
) : (
|
|
140
|
+
<span class="text-2xl font-black tracking-tighter text-[var(--kyro-text-primary)] ">
|
|
141
|
+
{siteName}
|
|
142
|
+
</span>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
104
145
|
</div>
|
|
105
146
|
|
|
106
|
-
<nav class="flex-1 px-4 overflow-y-auto">
|
|
147
|
+
<nav class="flex-1 px-4 overflow-y-auto" id="sidebar-nav">
|
|
107
148
|
<div class="space-y-4">
|
|
108
149
|
{
|
|
109
150
|
navSections.map((section) => (
|
|
110
|
-
<div class="space-y-
|
|
151
|
+
<div class="space-y-2" data-section={section.label}>
|
|
111
152
|
<div class="pt-4 pb-2">
|
|
112
|
-
<p class="px-6 text-[
|
|
153
|
+
<p class="px-6 text-[10px] font-medium text-[var(--kyro-text-secondary)] tracking-[0.2em] opacity-40">
|
|
113
154
|
{section.label}
|
|
114
155
|
</p>
|
|
115
156
|
</div>
|
|
116
|
-
{section.items.map((item) =>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
157
|
+
{section.items.map((item) => {
|
|
158
|
+
const slug = item.href.split("/").pop();
|
|
159
|
+
const type = item.href.includes("/settings")
|
|
160
|
+
? "global"
|
|
161
|
+
: "collection";
|
|
162
|
+
return (
|
|
163
|
+
<a
|
|
164
|
+
href={item.href}
|
|
165
|
+
data-nav-item
|
|
166
|
+
data-slug={slug}
|
|
167
|
+
data-type={type}
|
|
168
|
+
class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-medium text-[13px] ${
|
|
169
|
+
item.icon === "collection"
|
|
170
|
+
? currentPath === item.href ||
|
|
171
|
+
currentPath.startsWith(item.href + "/")
|
|
172
|
+
? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]"
|
|
173
|
+
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
174
|
+
: isActive(item)
|
|
175
|
+
? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-lg"
|
|
176
|
+
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
177
|
+
}`}
|
|
135
178
|
>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
</a>
|
|
145
|
-
))}
|
|
179
|
+
{(() => {
|
|
180
|
+
const Icon = icons[item.icon] || Database;
|
|
181
|
+
return <Icon className="w-4 h-4" strokeWidth={2.5} />;
|
|
182
|
+
})()}
|
|
183
|
+
<span>{item.label}</span>
|
|
184
|
+
</a>
|
|
185
|
+
);
|
|
186
|
+
})}
|
|
146
187
|
</div>
|
|
147
188
|
))
|
|
148
189
|
}
|
|
149
190
|
</div>
|
|
150
191
|
</nav>
|
|
151
192
|
|
|
193
|
+
<script>
|
|
194
|
+
// Handle frontend RBAC pruning in the sidebar
|
|
195
|
+
window.addEventListener("kyro:auth-ready", (event: any) => {
|
|
196
|
+
const { permissions } = event.detail;
|
|
197
|
+
if (!permissions) return;
|
|
198
|
+
|
|
199
|
+
const navItems = document.querySelectorAll("[data-nav-item]");
|
|
200
|
+
navItems.forEach((item: any) => {
|
|
201
|
+
const slug = item.getAttribute("data-slug");
|
|
202
|
+
const type = item.getAttribute("data-type");
|
|
203
|
+
|
|
204
|
+
let hasAccess = true;
|
|
205
|
+
|
|
206
|
+
if (type === "collection" && permissions.collections) {
|
|
207
|
+
const colPerms = permissions.collections[slug];
|
|
208
|
+
if (colPerms && colPerms.read === false) {
|
|
209
|
+
hasAccess = false;
|
|
210
|
+
}
|
|
211
|
+
} else if (type === "global" && permissions.globals) {
|
|
212
|
+
const globalPerms = permissions.globals[slug];
|
|
213
|
+
if (globalPerms && globalPerms.read === false) {
|
|
214
|
+
hasAccess = false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!hasAccess) {
|
|
219
|
+
item.style.display = "none";
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Hide empty sections
|
|
224
|
+
const sections = document.querySelectorAll("[data-section]");
|
|
225
|
+
sections.forEach((section: any) => {
|
|
226
|
+
const visibleItems = section.querySelectorAll(
|
|
227
|
+
'[data-nav-item]:not([style*="display: none"])',
|
|
228
|
+
);
|
|
229
|
+
if (visibleItems.length === 0) {
|
|
230
|
+
section.style.display = "none";
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
</script>
|
|
235
|
+
|
|
152
236
|
<div class="px-6 py-4 mt-auto">
|
|
153
237
|
<div
|
|
154
238
|
class="flex items-center justify-between p-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-2xl"
|
|
@@ -161,76 +245,20 @@ function isActive(item: NavItem): boolean {
|
|
|
161
245
|
class="p-2 rounded-lg transition-all active:scale-95"
|
|
162
246
|
title="Light Mode"
|
|
163
247
|
>
|
|
164
|
-
<
|
|
165
|
-
class="w-4 h-4"
|
|
166
|
-
fill="none"
|
|
167
|
-
stroke="currentColor"
|
|
168
|
-
viewBox="0 0 24 24"
|
|
169
|
-
>
|
|
170
|
-
<path
|
|
171
|
-
stroke-linecap="round"
|
|
172
|
-
stroke-linejoin="round"
|
|
173
|
-
stroke-width="2.5"
|
|
174
|
-
d={icons.sun}></path>
|
|
175
|
-
</svg>
|
|
248
|
+
<Sun className="w-4 h-4" strokeWidth={2.5} />
|
|
176
249
|
</button>
|
|
177
250
|
<button
|
|
178
251
|
id="theme-dark-btn"
|
|
179
252
|
class="p-2 rounded-lg transition-all active:scale-95"
|
|
180
253
|
title="Dark Mode"
|
|
181
254
|
>
|
|
182
|
-
<
|
|
183
|
-
class="w-4 h-4"
|
|
184
|
-
fill="none"
|
|
185
|
-
stroke="currentColor"
|
|
186
|
-
viewBox="0 0 24 24"
|
|
187
|
-
>
|
|
188
|
-
<path
|
|
189
|
-
stroke-linecap="round"
|
|
190
|
-
stroke-linejoin="round"
|
|
191
|
-
stroke-width="2.5"
|
|
192
|
-
d={icons.moon}></path>
|
|
193
|
-
</svg>
|
|
255
|
+
<Moon className="w-4 h-4" strokeWidth={2.5} />
|
|
194
256
|
</button>
|
|
195
257
|
</div>
|
|
196
258
|
|
|
197
259
|
<div class="flex items-center gap-2">
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
class="flex justify-center p-2.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] rounded-xl transition-all shadow-sm active:scale-95"
|
|
201
|
-
title="Account"
|
|
202
|
-
>
|
|
203
|
-
<svg
|
|
204
|
-
class="w-4 h-4"
|
|
205
|
-
fill="none"
|
|
206
|
-
stroke="currentColor"
|
|
207
|
-
viewBox="0 0 24 24"
|
|
208
|
-
>
|
|
209
|
-
<path
|
|
210
|
-
stroke-linecap="round"
|
|
211
|
-
stroke-linejoin="round"
|
|
212
|
-
stroke-width="2.5"
|
|
213
|
-
d={icons.user}></path>
|
|
214
|
-
</svg>
|
|
215
|
-
</a>
|
|
216
|
-
<button
|
|
217
|
-
id="logout-btn"
|
|
218
|
-
class="flex justify-center p-2.5 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all shadow-sm active:scale-95 font-semibold"
|
|
219
|
-
title="Logout"
|
|
220
|
-
>
|
|
221
|
-
<svg
|
|
222
|
-
class="w-4 h-4"
|
|
223
|
-
fill="none"
|
|
224
|
-
stroke="currentColor"
|
|
225
|
-
viewBox="0 0 24 24"
|
|
226
|
-
>
|
|
227
|
-
<path
|
|
228
|
-
stroke-linecap="round"
|
|
229
|
-
stroke-linejoin="round"
|
|
230
|
-
stroke-width="2.5"
|
|
231
|
-
d={icons.logout}></path>
|
|
232
|
-
</svg>
|
|
233
|
-
</button>
|
|
260
|
+
<UserMenu client:load adminPath={adminPath} />
|
|
261
|
+
<button id="logout-btn" class="hidden"></button>
|
|
234
262
|
</div>
|
|
235
263
|
</div>
|
|
236
264
|
</div>
|