@kyro-cms/admin 0.1.9 → 0.2.0

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/README.md CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  Admin dashboard for Kyro CMS — a React-based admin interface built with Astro.
4
4
 
5
+ Uses `@kyro-cms/utils` for API calls, date formatting, and validation.
6
+
5
7
  ## Features
6
8
 
7
9
  - **Multi-Database Support** — SQLite, PostgreSQL, MySQL, MongoDB
8
10
  - **Authentication** — JWT-based login/logout with auto-selecting auth adapter
11
+ - **Shared Utilities** — Uses @kyro-cms/utils for consistent API handling
9
12
  - **Collection Management** — Create, edit, and manage content collections
10
13
  - **User Management** — Manage users, roles, and permissions
11
14
  - **Settings** — Configure CMS settings, globals, and plugins
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyro-cms/admin",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -47,8 +47,8 @@
47
47
  "@dnd-kit/sortable": "^10.0.0",
48
48
  "@dnd-kit/utilities": "^3.2.2",
49
49
  "@graphiql/react": "^0.37.3",
50
- "@kyro-cms/core": "workspace:*",
51
- "@kyro-cms/utils": "workspace:*",
50
+ "@kyro-cms/core": "^0.2.0",
51
+ "@kyro-cms/utils": "^0.2.0",
52
52
  "@platejs/dnd": "^53.0.0",
53
53
  "@portabletext/editor": "^6.6.3",
54
54
  "@portabletext/react": "^6.0.3",
@@ -93,4 +93,4 @@
93
93
  "peerDependencies": {
94
94
  "@kyro-cms/core": "^0.1.2"
95
95
  }
96
- }
96
+ }
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect } from "react";
2
+ import { apiPost } from "@kyro-cms/utils/lib/api";
2
3
  import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
3
4
  import { ListView } from "./ListView";
4
5
  import { DetailView } from "./DetailView";
@@ -141,7 +142,7 @@ export function Admin({ config, theme = "light", onThemeChange }: AdminProps) {
141
142
 
142
143
  const handleLogout = async () => {
143
144
  try {
144
- await fetch("/api/auth/logout", { method: "POST" });
145
+ await apiPost("/api/auth/logout");
145
146
  } catch {
146
147
  } finally {
147
148
  localStorage.removeItem("kyro_token");
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
+ import { apiGet, apiPost, apiDelete } from "@kyro-cms/utils/lib/api";
2
3
  import {
3
4
  Key,
4
5
  Plus,
@@ -44,17 +45,14 @@ export function ApiKeysManager() {
44
45
  const loadKeys = async () => {
45
46
  setLoading(true);
46
47
  try {
47
- const res = await fetch("/api/keys");
48
- if (res.ok) {
49
- const data = await res.json();
50
- setKeys(
51
- data.map((k: any) => ({
52
- ...k,
53
- key: k.key,
54
- keyPrefix: k.keyPrefix || k.key?.substring(0, 8) || "",
55
- })),
56
- );
57
- }
48
+ const data = await apiGet("/api/keys");
49
+ setKeys(
50
+ data.map((k: any) => ({
51
+ ...k,
52
+ key: k.key,
53
+ keyPrefix: k.keyPrefix || k.key?.substring(0, 8) || "",
54
+ })),
55
+ );
58
56
  } catch (e) {
59
57
  console.error(e);
60
58
  } finally {
@@ -73,23 +71,12 @@ export function ApiKeysManager() {
73
71
  }
74
72
 
75
73
  try {
76
- const res = await fetch("/api/keys", {
77
- method: "POST",
78
- headers: { "Content-Type": "application/json" },
79
- body: JSON.stringify({ name: newKeyName }),
80
- });
81
-
82
- if (res.ok) {
83
- const created = await res.json();
84
- setNewKey(created);
85
- setShowCreateModal(false);
86
- setNewKeyName("");
87
- setCreateError("");
88
- loadKeys();
89
- } else {
90
- const error = await res.json();
91
- setCreateError(error.error || "Failed to create API key");
92
- }
74
+ const created = await apiPost("/api/keys", { name: newKeyName });
75
+ setNewKey(created);
76
+ setShowCreateModal(false);
77
+ setNewKeyName("");
78
+ setCreateError("");
79
+ loadKeys();
93
80
  } catch (e) {
94
81
  console.error(e);
95
82
  setCreateError("Failed to create API key");
@@ -105,13 +92,8 @@ export function ApiKeysManager() {
105
92
  if (!deleteKeyId) return;
106
93
 
107
94
  try {
108
- const res = await fetch(`/api/keys/${deleteKeyId}`, { method: "DELETE" });
109
- if (res.ok) {
110
- loadKeys();
111
- } else {
112
- setAlertMessage("Failed to delete API key");
113
- setShowAlertModal(true);
114
- }
95
+ await apiDelete(`/api/keys/${deleteKeyId}`);
96
+ loadKeys();
115
97
  } catch (e) {
116
98
  console.error(e);
117
99
  setAlertMessage("Failed to delete API key");
@@ -136,7 +118,8 @@ export function ApiKeysManager() {
136
118
  <h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
137
119
  API <span className="text-[var(--kyro-primary)]">Keys</span>
138
120
  </h1>
139
- <button type="button"
121
+ <button
122
+ type="button"
140
123
  onClick={() => setShowHelpModal(true)}
141
124
  className="p-2 rounded-lg text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
142
125
  title="Learn how API keys work"
@@ -148,7 +131,8 @@ export function ApiKeysManager() {
148
131
  Secure tokens for authenticating API requests.
149
132
  </p>
150
133
  </div>
151
- <button type="button"
134
+ <button
135
+ type="button"
152
136
  onClick={() => {
153
137
  setNewKeyName("");
154
138
  setCreateError("");
@@ -187,7 +171,8 @@ export function ApiKeysManager() {
187
171
  -H "Authorization: ApiKey kyro_xxx"
188
172
  </div>
189
173
  </div>
190
- <button type="button"
174
+ <button
175
+ type="button"
191
176
  onClick={() => {
192
177
  navigator.clipboard.writeText(
193
178
  'curl -X GET https://yoursite.com/api/posts -H "Authorization: ApiKey YOUR_KEY"',
@@ -276,7 +261,8 @@ export function ApiKeysManager() {
276
261
  <code className="flex-1 p-4 bg-[var(--kyro-bg)] border border-green-500/30 rounded-xl font-mono text-sm break-all">
277
262
  {newKey.key}
278
263
  </code>
279
- <button type="button"
264
+ <button
265
+ type="button"
280
266
  onClick={() => copyToClipboard(newKey.key!, newKey.id)}
281
267
  className="p-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl hover:opacity-90 flex-shrink-0"
282
268
  title="Copy to clipboard"
@@ -290,7 +276,8 @@ export function ApiKeysManager() {
290
276
  </div>
291
277
  </div>
292
278
  </div>
293
- <button type="button"
279
+ <button
280
+ type="button"
294
281
  onClick={() => setNewKey(null)}
295
282
  className="mt-6 text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
296
283
  >
@@ -319,7 +306,8 @@ export function ApiKeysManager() {
319
306
  <p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-6">
320
307
  Create your first API key to authenticate with the API.
321
308
  </p>
322
- <button type="button"
309
+ <button
310
+ type="button"
323
311
  onClick={() => setShowCreateModal(true)}
324
312
  className="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm hover:opacity-90 transition-all"
325
313
  >
@@ -363,7 +351,8 @@ export function ApiKeysManager() {
363
351
  </div>
364
352
  </div>
365
353
  <div className="flex items-center gap-2">
366
- <button type="button"
354
+ <button
355
+ type="button"
367
356
  onClick={() =>
368
357
  copyToClipboard(
369
358
  key.key || `${key.keyPrefix}...`,
@@ -381,7 +370,8 @@ export function ApiKeysManager() {
381
370
  {copiedId === key.id ? "Copied!" : "Copy"}
382
371
  </span>
383
372
  </button>
384
- <button type="button"
373
+ <button
374
+ type="button"
385
375
  onClick={() => handleDeleteKey(key.id)}
386
376
  className="p-3 text-red-500 bg-red-500/10 rounded-xl hover:bg-red-500/20"
387
377
  title="Delete API key"
@@ -433,13 +423,15 @@ export function ApiKeysManager() {
433
423
  </div>
434
424
  </ModalContent>
435
425
  <ModalActions>
436
- <button type="button"
426
+ <button
427
+ type="button"
437
428
  onClick={() => setShowCreateModal(false)}
438
429
  className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)] transition-colors"
439
430
  >
440
431
  Cancel
441
432
  </button>
442
- <button type="button"
433
+ <button
434
+ type="button"
443
435
  onClick={handleCreateKey}
444
436
  className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
445
437
  >
@@ -468,13 +460,15 @@ export function ApiKeysManager() {
468
460
  </div>
469
461
  </ModalContent>
470
462
  <ModalActions>
471
- <button type="button"
463
+ <button
464
+ type="button"
472
465
  onClick={() => setShowDeleteModal(false)}
473
466
  className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)] transition-colors"
474
467
  >
475
468
  Keep Key
476
469
  </button>
477
- <button type="button"
470
+ <button
471
+ type="button"
478
472
  onClick={confirmDeleteKey}
479
473
  className="px-4 py-2 rounded-lg font-medium text-sm bg-red-500 text-white hover:bg-red-600 transition-colors"
480
474
  >
@@ -529,7 +523,8 @@ export function ApiKeysManager() {
529
523
  </div>
530
524
  </ModalContent>
531
525
  <ModalActions>
532
- <button type="button"
526
+ <button
527
+ type="button"
533
528
  onClick={() => setShowHelpModal(false)}
534
529
  className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
535
530
  >
@@ -550,7 +545,8 @@ export function ApiKeysManager() {
550
545
  </p>
551
546
  </ModalContent>
552
547
  <ModalActions>
553
- <button type="button"
548
+ <button
549
+ type="button"
554
550
  onClick={() => setShowAlertModal(false)}
555
551
  className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
556
552
  >
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect, useCallback } from "react";
2
+ import { apiGet } from "@kyro-cms/utils/lib/api";
2
3
  import { Modal } from "./ui/Modal";
3
4
 
4
5
  interface AuditLog {
@@ -283,7 +284,8 @@ export function AuditLogsPage() {
283
284
  </select>
284
285
 
285
286
  {(search || action || successFilter) && (
286
- <button type="button"
287
+ <button
288
+ type="button"
287
289
  onClick={() => {
288
290
  setSearch("");
289
291
  setAction("");
@@ -491,7 +493,8 @@ export function AuditLogsPage() {
491
493
  </span>
492
494
  </p>
493
495
  <div className="flex items-center gap-2">
494
- <button type="button"
496
+ <button
497
+ type="button"
495
498
  onClick={() => fetchLogs(page - 1)}
496
499
  disabled={page <= 1}
497
500
  className={`px-4 py-2 rounded-xl text-sm font-bold transition-all ${
@@ -505,7 +508,8 @@ export function AuditLogsPage() {
505
508
  {Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
506
509
  const p = i + 1;
507
510
  return (
508
- <button type="button"
511
+ <button
512
+ type="button"
509
513
  key={p}
510
514
  onClick={() => fetchLogs(p)}
511
515
  className={`px-3.5 py-2 rounded-xl text-sm font-bold transition-all ${
@@ -518,7 +522,8 @@ export function AuditLogsPage() {
518
522
  </button>
519
523
  );
520
524
  })}
521
- <button type="button"
525
+ <button
526
+ type="button"
522
527
  onClick={() => fetchLogs(page + 1)}
523
528
  disabled={page >= totalPages}
524
529
  className={`px-4 py-2 rounded-xl text-sm font-bold transition-all ${
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
+ import { apiGet, apiPatch } from "@kyro-cms/utils/lib/api";
2
3
  import {
3
4
  Palette,
4
5
  Tag,
@@ -24,17 +25,14 @@ export function BrandingHub() {
24
25
  useEffect(() => {
25
26
  const fetchBranding = async () => {
26
27
  try {
27
- const res = await fetch("/api/globals/site");
28
- if (res.ok) {
29
- const result = await res.json();
30
- const data = result.data || result;
31
- if (data && Object.keys(data).length > 0) {
32
- if (data.siteName) setSiteName(data.siteName);
33
- if (data.adminTitle) setAdminTitle(data.adminTitle);
34
- if (data.primaryColor) setPrimaryColor(data.primaryColor);
35
- if (data.dashboardGreeting)
36
- setDashboardGreeting(data.dashboardGreeting);
37
- }
28
+ const result = await apiGet("/api/globals/site");
29
+ const data = result.data || result;
30
+ if (data && Object.keys(data).length > 0) {
31
+ if (data.siteName) setSiteName(data.siteName);
32
+ if (data.adminTitle) setAdminTitle(data.adminTitle);
33
+ if (data.primaryColor) setPrimaryColor(data.primaryColor);
34
+ if (data.dashboardGreeting)
35
+ setDashboardGreeting(data.dashboardGreeting);
38
36
  }
39
37
  } catch (err) {
40
38
  console.error("Failed to load branding:", err);
@@ -46,26 +44,18 @@ export function BrandingHub() {
46
44
  const handleSave = async () => {
47
45
  setSaving(true);
48
46
  try {
49
- const res = await fetch("/api/globals/site", {
50
- method: "PATCH",
51
- headers: { "Content-Type": "application/json" },
52
- body: JSON.stringify({
53
- siteName,
54
- adminTitle,
55
- primaryColor,
56
- dashboardGreeting,
57
- }),
47
+ await apiPatch("/api/globals/site", {
48
+ siteName,
49
+ adminTitle,
50
+ primaryColor,
51
+ dashboardGreeting,
58
52
  });
59
- if (res.ok) {
60
- setSaved(true);
61
- setTimeout(() => setSaved(false), 3000);
62
- document.documentElement.style.setProperty(
63
- "--kyro-primary",
64
- primaryColor,
65
- );
66
- } else {
67
- throw new Error("Failed to save");
68
- }
53
+ setSaved(true);
54
+ setTimeout(() => setSaved(false), 3000);
55
+ document.documentElement.style.setProperty(
56
+ "--kyro-primary",
57
+ primaryColor,
58
+ );
69
59
  } catch (e) {
70
60
  console.error(e);
71
61
  } finally {
@@ -96,7 +86,8 @@ export function BrandingHub() {
96
86
  </p>
97
87
  </div>
98
88
  <div className="flex items-center gap-3">
99
- <button type="button"
89
+ <button
90
+ type="button"
100
91
  onClick={handleSave}
101
92
  disabled={saving}
102
93
  className={`flex items-center gap-2 px-8 py-3 rounded-2xl font-black text-sm shadow-xl transition-all active:scale-95 ${
@@ -187,7 +178,8 @@ export function BrandingHub() {
187
178
  </label>
188
179
  <div className="grid grid-cols-6 gap-3">
189
180
  {colors.map((c) => (
190
- <button type="button"
181
+ <button
182
+ type="button"
191
183
  key={c.name}
192
184
  onClick={() => setPrimaryColor(c.hex)}
193
185
  className={`aspect-square rounded-xl transition-all border-4 ${primaryColor === c.hex ? "border-white ring-2 ring-[var(--kyro-primary)]" : "border-transparent opacity-60 hover:opacity-100"}`}
@@ -1,4 +1,5 @@
1
1
  import { useState } from "react";
2
+ import { apiPost } from "@kyro-cms/utils/lib/api";
2
3
  import type { KyroConfig, CollectionConfig } from "@kyro-cms/core/client";
3
4
  import { AutoForm } from "./AutoForm";
4
5
  import { Spinner } from "./ui/Spinner";
@@ -28,17 +29,7 @@ export function CreateView({
28
29
  e.preventDefault();
29
30
  try {
30
31
  setSaving(true);
31
- const response = await fetch(`/api/${collection.slug}`, {
32
- method: "POST",
33
- headers: { "Content-Type": "application/json" },
34
- body: JSON.stringify(data),
35
- });
36
-
37
- if (!response.ok) {
38
- const error = await response.json();
39
- throw new Error(error.message || "Failed to create");
40
- }
41
-
32
+ await apiPost(`/api/${collection.slug}`, data);
42
33
  onSuccess();
43
34
  } catch (err) {
44
35
  onError(err instanceof Error ? err.message : "Failed to create");
@@ -64,14 +55,16 @@ export function CreateView({
64
55
  </button>
65
56
  <h2 className="kyro-detail-title">Create {label}</h2>
66
57
  <div className="kyro-detail-actions">
67
- <button type="button"
58
+ <button
59
+ type="button"
68
60
  className="kyro-btn kyro-btn-secondary kyro-btn-md"
69
61
  onClick={onCancel}
70
62
  disabled={saving}
71
63
  >
72
64
  Cancel
73
65
  </button>
74
- <button type="button"
66
+ <button
67
+ type="button"
75
68
  className="kyro-btn kyro-btn-primary kyro-btn-md"
76
69
  onClick={handleSubmit}
77
70
  disabled={saving}
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
+ import { apiGet, apiPatch, apiPost, apiDelete } from "@kyro-cms/utils/lib/api";
2
3
  import type {
3
4
  KyroConfig,
4
5
  CollectionConfig,
@@ -107,9 +108,7 @@ export function DetailView({
107
108
  const loadDocument = async () => {
108
109
  try {
109
110
  setLoading(true);
110
- const response = await fetch(`/api/${slug}/${documentId}`);
111
- if (!response.ok) throw new Error("Failed to load document");
112
- const result = await response.json();
111
+ const result = await apiGet(`/api/${slug}/${documentId}`);
113
112
  const docData = result.data || {};
114
113
  setData(docData);
115
114
  setOriginalData(docData);
@@ -127,9 +126,7 @@ export function DetailView({
127
126
  const loadGlobal = async () => {
128
127
  try {
129
128
  setLoading(true);
130
- const response = await fetch(`/api/globals/${slug}`);
131
- if (!response.ok) throw new Error("Failed to load global");
132
- const result = await response.json();
129
+ const result = await apiGet(`/api/globals/${slug}`);
133
130
  const globalData = result.data || {};
134
131
  setData(globalData);
135
132
  setOriginalData(globalData);
@@ -151,13 +148,7 @@ export function DetailView({
151
148
  ? `/api/globals/${slug}`
152
149
  : `/api/${slug}/${documentId}`;
153
150
 
154
- const response = await fetch(endpoint, {
155
- method: "PATCH",
156
- headers: { "Content-Type": "application/json" },
157
- body: JSON.stringify(data),
158
- });
159
-
160
- if (!response.ok) throw new Error("Failed to save");
151
+ await apiPatch(endpoint, data);
161
152
 
162
153
  if (!isAutosave) {
163
154
  setOriginalData(data);
@@ -193,10 +184,7 @@ export function DetailView({
193
184
  const handlePublish = async () => {
194
185
  try {
195
186
  setSaving(true);
196
- const response = await fetch(`/api/${slug}/${documentId}/publish`, {
197
- method: "POST",
198
- });
199
- if (!response.ok) throw new Error("Failed to publish");
187
+ await apiPost(`/api/${slug}/${documentId}/publish`);
200
188
  setStatus("published");
201
189
  setPublishedAt(new Date().toISOString());
202
190
  addToast("success", "Published successfully");
@@ -211,10 +199,7 @@ export function DetailView({
211
199
  const handleUnpublish = async () => {
212
200
  try {
213
201
  setSaving(true);
214
- const response = await fetch(`/api/${slug}/${documentId}/unpublish`, {
215
- method: "POST",
216
- });
217
- if (!response.ok) throw new Error("Failed to unpublish");
202
+ await apiPost(`/api/${slug}/${documentId}/unpublish`);
218
203
  setStatus("draft");
219
204
  } catch {
220
205
  onError("Failed to unpublish");
@@ -225,10 +210,7 @@ export function DetailView({
225
210
 
226
211
  const handleDuplicate = async () => {
227
212
  try {
228
- const response = await fetch(`/api/${slug}/${documentId}/duplicate`, {
229
- method: "POST",
230
- });
231
- if (!response.ok) throw new Error("Failed to duplicate");
213
+ await apiPost(`/api/${slug}/${documentId}/duplicate`);
232
214
  onError("Document duplicated successfully");
233
215
  } catch {
234
216
  onError("Failed to duplicate document");
@@ -238,11 +220,7 @@ export function DetailView({
238
220
  const handleDelete = async () => {
239
221
  try {
240
222
  setDeleting(true);
241
- const response = await fetch(`/api/${slug}/${documentId}`, {
242
- method: "DELETE",
243
- });
244
-
245
- if (!response.ok) throw new Error("Failed to delete");
223
+ await apiDelete(`/api/${slug}/${documentId}`);
246
224
  onDelete?.();
247
225
  } catch {
248
226
  onError("Failed to delete document");