@kyro-cms/admin 0.3.2 → 0.3.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.
Files changed (242) hide show
  1. package/dist/EditorClient-XEUOVAAC.js +466 -0
  2. package/dist/EditorClient-XEUOVAAC.js.map +1 -0
  3. package/dist/EditorClient-YLCGVDXY.cjs +468 -0
  4. package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
  5. package/dist/chunk-7KPIUCGT.js +384 -0
  6. package/dist/chunk-7KPIUCGT.js.map +1 -0
  7. package/dist/chunk-GOACG6R7.cjs +473 -0
  8. package/dist/chunk-GOACG6R7.cjs.map +1 -0
  9. package/dist/index.cjs +14861 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +1661 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.ts +563 -0
  14. package/dist/index.js +14784 -0
  15. package/dist/index.js.map +1 -0
  16. package/package.json +19 -19
  17. package/src/components/ActionBar.tsx +7 -43
  18. package/src/components/Admin.tsx +138 -277
  19. package/src/components/ApiKeysManager.tsx +428 -419
  20. package/src/components/AuditLogsPage.tsx +35 -39
  21. package/src/components/AuthBridge.tsx +51 -0
  22. package/src/components/AutoForm.tsx +495 -1230
  23. package/src/components/BrandingHub.tsx +18 -19
  24. package/src/components/BulkActionsBar.tsx +1 -1
  25. package/src/components/CreateView.tsx +22 -36
  26. package/src/components/Dashboard.tsx +60 -84
  27. package/src/components/DetailView.tsx +113 -91
  28. package/src/components/DeveloperCenter.tsx +200 -198
  29. package/src/components/FieldRenderer.tsx +206 -0
  30. package/src/components/GraphQLPlayground.tsx +340 -480
  31. package/src/components/ListView.tsx +828 -254
  32. package/src/components/LoginPage.tsx +3 -4
  33. package/src/components/MarketplaceManager.tsx +254 -0
  34. package/src/components/MediaGallery.tsx +856 -1192
  35. package/src/components/PluginsManager.tsx +277 -0
  36. package/src/components/RestPlayground.tsx +398 -560
  37. package/src/components/SessionsManager.tsx +211 -0
  38. package/src/components/Sidebar.astro +179 -151
  39. package/src/components/ThemeProvider.tsx +7 -161
  40. package/src/components/UserManagement.tsx +162 -146
  41. package/src/components/UserMenu.tsx +110 -0
  42. package/src/components/WebhookManager.tsx +305 -367
  43. package/src/components/blocks/AccordionBlock.tsx +4 -4
  44. package/src/components/blocks/ArrayBlock.tsx +3 -3
  45. package/src/components/blocks/BlockEditModal.tsx +8 -8
  46. package/src/components/blocks/BlockWrapper.tsx +61 -0
  47. package/src/components/blocks/ButtonBlock.tsx +4 -4
  48. package/src/components/blocks/ChildBlocksTree.tsx +23 -25
  49. package/src/components/blocks/CodeBlock.tsx +15 -15
  50. package/src/components/blocks/ColumnsBlock.tsx +6 -44
  51. package/src/components/blocks/DividerBlock.tsx +3 -3
  52. package/src/components/blocks/FileBlock.tsx +4 -4
  53. package/src/components/blocks/HeadingBlock.tsx +6 -38
  54. package/src/components/blocks/HeroBlock.tsx +4 -4
  55. package/src/components/blocks/ImageBlock.tsx +4 -4
  56. package/src/components/blocks/LinkBlock.tsx +4 -4
  57. package/src/components/blocks/ListBlock.tsx +3 -3
  58. package/src/components/blocks/ParagraphBlock.tsx +12 -42
  59. package/src/components/blocks/RelationshipBlock.tsx +4 -4
  60. package/src/components/blocks/RichTextBlock.tsx +4 -4
  61. package/src/components/blocks/VStackBlock.tsx +5 -37
  62. package/src/components/blocks/VideoBlock.tsx +4 -4
  63. package/src/components/blocks/types.ts +11 -0
  64. package/src/components/fields/AccordionField.tsx +1 -1
  65. package/src/components/fields/ArrayField.tsx +2 -2
  66. package/src/components/fields/ArrayLayout.tsx +93 -0
  67. package/src/components/fields/BlocksField.tsx +122 -111
  68. package/src/components/fields/ButtonField.tsx +1 -1
  69. package/src/components/fields/CheckboxField.tsx +14 -15
  70. package/src/components/fields/ChildrenField.tsx +2 -2
  71. package/src/components/fields/CodeField.tsx +3 -3
  72. package/src/components/fields/ColumnsField.tsx +2 -2
  73. package/src/components/fields/DateField.tsx +13 -26
  74. package/src/components/fields/EditorClient.tsx +26 -28
  75. package/src/components/fields/FieldLayout.tsx +52 -0
  76. package/src/components/fields/GroupLayout.tsx +35 -0
  77. package/src/components/fields/JSONField.tsx +7 -7
  78. package/src/components/fields/LinkField.tsx +1 -1
  79. package/src/components/fields/MarkdownField.tsx +1 -1
  80. package/src/components/fields/NumberField.tsx +13 -26
  81. package/src/components/fields/PortableTextField.tsx +4 -4
  82. package/src/components/fields/PortableTextRenderer.tsx +1 -1
  83. package/src/components/fields/RelationshipBlockField.tsx +31 -23
  84. package/src/components/fields/RelationshipField.tsx +14 -14
  85. package/src/components/fields/SelectField.tsx +17 -26
  86. package/src/components/fields/TabsLayout.tsx +69 -0
  87. package/src/components/fields/TextField.tsx +85 -38
  88. package/src/components/fields/UploadField.tsx +71 -41
  89. package/src/components/fields/VideoField.tsx +1 -1
  90. package/src/components/fields/extensions/blockComponents.tsx +2 -2
  91. package/src/components/fields/extensions/blocksStore.ts +207 -193
  92. package/src/components/fields/types.ts +22 -0
  93. package/src/components/layout/Layout.tsx +1 -1
  94. package/src/components/ui/ActionMenu.tsx +63 -0
  95. package/src/components/ui/Badge.tsx +59 -5
  96. package/src/components/ui/BlockDrawer.tsx +4 -5
  97. package/src/components/ui/CommandPalette.tsx +58 -36
  98. package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
  99. package/src/components/ui/Dropdown.tsx +18 -16
  100. package/src/components/ui/EmptyState.tsx +25 -0
  101. package/src/components/ui/GlobalModal.tsx +49 -0
  102. package/src/components/ui/IconButton.tsx +44 -0
  103. package/src/components/ui/Modal.tsx +19 -20
  104. package/src/components/ui/PageHeader.tsx +158 -0
  105. package/src/components/ui/Pagination.tsx +61 -0
  106. package/src/components/ui/PromptModal.tsx +1 -1
  107. package/src/components/ui/SearchInput.tsx +57 -0
  108. package/src/components/ui/SeoPreview.tsx +31 -0
  109. package/src/components/ui/SessionModal.tsx +0 -0
  110. package/src/components/ui/SlidePanel.tsx +2 -0
  111. package/src/components/ui/Toast.tsx +65 -122
  112. package/src/components/ui/Toaster.tsx +18 -0
  113. package/src/components/ui/icons.tsx +112 -0
  114. package/src/components/users/UserDetail.tsx +290 -0
  115. package/src/components/users/UserForm.tsx +242 -0
  116. package/src/components/users/UsersList.tsx +338 -0
  117. package/src/env.d.ts +13 -13
  118. package/src/fields/index.ts +2 -1
  119. package/src/global.d.ts +7 -0
  120. package/src/hooks/data.ts +2 -9
  121. package/src/hooks/useAsyncData.ts +36 -0
  122. package/src/hooks/useAutoFormState.ts +527 -0
  123. package/src/hooks/useSelection.ts +49 -0
  124. package/src/hooks/useSession.ts +0 -0
  125. package/src/index.ts +11 -1
  126. package/src/integration.ts +86 -11
  127. package/src/kyro-cms.d.ts +209 -0
  128. package/src/layouts/AdminLayout.astro +128 -11
  129. package/src/layouts/AuthLayout.astro +21 -5
  130. package/src/lib/api.ts +175 -55
  131. package/src/lib/autoform-store.ts +435 -0
  132. package/src/lib/config.ts +82 -34
  133. package/src/lib/createRegistry.ts +29 -0
  134. package/src/lib/default-kyro-config.ts +4 -0
  135. package/src/lib/globals.ts +50 -0
  136. package/src/lib/media-utils.ts +18 -0
  137. package/src/lib/object-utils.ts +77 -0
  138. package/src/lib/paths.ts +61 -0
  139. package/src/lib/stores/index.ts +370 -0
  140. package/src/lib/types.ts +43 -0
  141. package/src/lib/useResourceManager.ts +105 -0
  142. package/src/pages/403.astro +67 -0
  143. package/src/pages/[collection]/[id].astro +14 -180
  144. package/src/pages/[collection]/index.astro +11 -6
  145. package/src/pages/api-explorer.astro +173 -0
  146. package/src/pages/audit/index.astro +2 -0
  147. package/src/pages/auth/login.astro +122 -0
  148. package/src/pages/auth/register.astro +167 -0
  149. package/src/pages/graphql-explorer.astro +59 -0
  150. package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
  151. package/src/pages/index.astro +577 -0
  152. package/src/pages/index_ALT.astro +3 -0
  153. package/src/pages/keys.astro +11 -0
  154. package/src/pages/marketplace.astro +11 -0
  155. package/src/pages/media.astro +3 -0
  156. package/src/pages/plugins.astro +8 -0
  157. package/src/pages/preview/[collection]/[id].astro +188 -123
  158. package/src/pages/rest-playground.astro +62 -0
  159. package/src/pages/roles/index.astro +183 -76
  160. package/src/pages/sessions.astro +8 -0
  161. package/src/pages/settings/[slug].astro +92 -114
  162. package/src/pages/settings/index.astro +5 -3
  163. package/src/pages/users/[id].astro +25 -154
  164. package/src/pages/users/index.astro +19 -130
  165. package/src/pages/users/new.astro +9 -86
  166. package/src/pages/webhooks.astro +11 -0
  167. package/src/routes.ts +80 -0
  168. package/src/styles/main.css +119 -79
  169. package/src/theme/tokens.ts +1 -0
  170. package/src/vite-env.d.ts +14 -0
  171. package/src/collections/auth/index.ts +0 -155
  172. package/src/collections/portfolio/index.ts +0 -343
  173. package/src/components/ApiExplorer.tsx +0 -325
  174. package/src/components/EnhancedListView.tsx +0 -889
  175. package/src/components/GraphQLExplorer.tsx +0 -675
  176. package/src/components/Icons.tsx +0 -23
  177. package/src/components/StatusBadge.tsx +0 -76
  178. package/src/lib/MediaService.ts +0 -541
  179. package/src/lib/auth/sqlite-adapter.ts +0 -319
  180. package/src/lib/dataStore.ts +0 -226
  181. package/src/lib/db/adapter.ts +0 -54
  182. package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
  183. package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
  184. package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
  185. package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
  186. package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
  187. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
  188. package/src/lib/db/index.ts +0 -449
  189. package/src/lib/db/mongodb-adapter.ts +0 -207
  190. package/src/lib/db/mongodb-auth-adapter.ts +0 -305
  191. package/src/lib/db/schema/mysql-auth.ts +0 -113
  192. package/src/lib/db/schema/mysql-content.ts +0 -20
  193. package/src/lib/db/schema/postgres-auth.ts +0 -116
  194. package/src/lib/db/schema/postgres-content.ts +0 -35
  195. package/src/lib/db/schema/postgres-media.ts +0 -52
  196. package/src/lib/db/schema/postgres-settings.ts +0 -11
  197. package/src/lib/db/schema/sqlite-auth.ts +0 -112
  198. package/src/lib/db/schema/sqlite-content.ts +0 -20
  199. package/src/lib/db/version-adapter.ts +0 -248
  200. package/src/lib/graphql/index.ts +0 -1
  201. package/src/lib/graphql/schema.ts +0 -443
  202. package/src/lib/rate-limit.ts +0 -267
  203. package/src/lib/storage.ts +0 -374
  204. package/src/lib/store.ts +0 -85
  205. package/src/middleware.ts +0 -177
  206. package/src/pages/admin/api-explorer.astro +0 -98
  207. package/src/pages/admin/graphql-explorer.astro +0 -40
  208. package/src/pages/admin/index.astro +0 -286
  209. package/src/pages/admin/keys.astro +0 -8
  210. package/src/pages/admin/rest-playground.astro +0 -44
  211. package/src/pages/admin/webhooks.astro +0 -8
  212. package/src/pages/api/[collection]/[id]/publish.ts +0 -52
  213. package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
  214. package/src/pages/api/[collection]/[id]/versions.ts +0 -66
  215. package/src/pages/api/[collection]/[id].ts +0 -213
  216. package/src/pages/api/[collection]/index.ts +0 -209
  217. package/src/pages/api/auth/[id].ts +0 -121
  218. package/src/pages/api/auth/audit-logs.ts +0 -57
  219. package/src/pages/api/auth/login.ts +0 -211
  220. package/src/pages/api/auth/logout.ts +0 -66
  221. package/src/pages/api/auth/me.ts +0 -36
  222. package/src/pages/api/auth/refresh.ts +0 -119
  223. package/src/pages/api/auth/register.ts +0 -188
  224. package/src/pages/api/auth/users.ts +0 -97
  225. package/src/pages/api/collections.ts +0 -59
  226. package/src/pages/api/globals/[slug].ts +0 -42
  227. package/src/pages/api/graphql.ts +0 -90
  228. package/src/pages/api/health.ts +0 -426
  229. package/src/pages/api/keys/[id].ts +0 -26
  230. package/src/pages/api/keys/index.ts +0 -75
  231. package/src/pages/api/media/[id].ts +0 -309
  232. package/src/pages/api/media/folders.ts +0 -609
  233. package/src/pages/api/media/index.ts +0 -146
  234. package/src/pages/api/media/resize.ts +0 -267
  235. package/src/pages/api/search.ts +0 -82
  236. package/src/pages/api/slug-availability.ts +0 -70
  237. package/src/pages/api/storage-config.ts +0 -20
  238. package/src/pages/api/storage-status.ts +0 -206
  239. package/src/pages/api/upload.ts +0 -334
  240. package/src/pages/api/webhooks/index.ts +0 -71
  241. package/src/pages/login.astro +0 -82
  242. 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 "@/lib/config";
4
-
5
- interface User {
6
- id: string;
7
- email: string;
8
- role: string;
9
- tenantId?: string;
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, user } = Astro.props;
44
+ const { title } = Astro.props;
24
45
  const currentPath = Astro.url.pathname;
25
46
 
26
- const collectionItems: NavItem[] = nonAuthCollections.map((col) => ({
27
- href: `/${col.slug}`,
28
- label: col.label || col.slug,
29
- icon: "collection",
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: [{ href: "/", label: "Dashboard", icon: "home" }],
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: "/settings", label: "General Settings", icon: "settings" },
45
- { href: "/users", label: "Users", icon: "users" },
46
- { href: "/roles", label: "Roles", icon: "roles" },
47
- { href: "/audit", label: "Audit Logs", icon: "audit" },
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, string> = {
60
- home: "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
- collection: "M12 12m-5 0a5 5 0 1 1 10 0 5 5 0 1 1-10 0",
62
- settings:
63
- "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37M15 12a3 3 0 11-6 0 3 3 0 016 0z",
64
- users:
65
- "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
66
- roles:
67
- "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",
68
- audit:
69
- "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
70
- keys: "M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z",
71
- webhooks:
72
- "M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1",
73
- user: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z",
74
- logout:
75
- "M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1",
76
- sun: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.364 17.636l-.707.707M17.636 17.636l-.707-.707M6.364 6.364l-.707-.707M15 12a3 3 0 11-6 0 3 3 0 016 0z",
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 === "/") return title === "Dashboard";
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 === "Roles";
86
- if (item.href === "/audit") return title === "Audit Logs";
87
- if (item.href === "/admin/keys") return title === "API Keys";
88
- if (item.href === "/admin/webhooks") return title === "Webhooks";
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-4 py-8">
100
- <span
101
- class="text-3xl font-black tracking-tighter text-[var(--kyro-text-primary)]"
102
- >KYRO.</span
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-1">
151
+ <div class="space-y-2" data-section={section.label}>
111
152
  <div class="pt-4 pb-2">
112
- <p class="px-6 text-[9px] font-black text-[var(--kyro-text-secondary)] uppercase tracking-[0.2em] opacity-40">
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
- <a
118
- href={item.href}
119
- class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-semibold ${
120
- item.icon === "collection"
121
- ? currentPath === item.href ||
122
- currentPath.startsWith(item.href + "/")
123
- ? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]"
124
- : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
125
- : isActive(item)
126
- ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-lg"
127
- : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
128
- }`}
129
- >
130
- <svg
131
- class="w-5 h-5"
132
- fill="none"
133
- stroke="currentColor"
134
- viewBox="0 0 24 24"
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
- <path
137
- stroke-linecap="round"
138
- stroke-linejoin="round"
139
- stroke-width="2.5"
140
- d={icons[item.icon]}
141
- />
142
- </svg>
143
- <span>{item.label}</span>
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
- <svg
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
- <svg
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
- <a
199
- href={user ? `/users/${user.id}` : "#"}
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>