@kyro-cms/admin 0.1.6 → 0.1.7
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 +149 -51
- package/package.json +53 -6
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +136 -27
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +23 -6
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/middleware.ts +70 -11
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +200 -139
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +42 -24
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +11 -11
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +13 -13
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +553 -128
- package/src/components/layout/Sidebar.tsx +0 -497
|
@@ -1,110 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import AdminLayout from '../../layouts/AdminLayout.astro';
|
|
3
|
-
|
|
4
|
-
let auditLogs: any[] = [];
|
|
5
|
-
let totalLogs = 0;
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
const response = await fetch(`${Astro.url.origin}/api/audit_logs?page=1&limit=50`);
|
|
9
|
-
if (response.ok) {
|
|
10
|
-
const data = await response.json();
|
|
11
|
-
auditLogs = data.docs || [];
|
|
12
|
-
totalLogs = data.totalDocs || 0;
|
|
13
|
-
}
|
|
14
|
-
} catch (error) {
|
|
15
|
-
console.error('Failed to fetch audit logs:', error);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const actionColors: Record<string, string> = {
|
|
19
|
-
login: 'bg-green-50 text-green-600',
|
|
20
|
-
logout: 'bg-gray-50 text-gray-600',
|
|
21
|
-
login_failed: 'bg-red-50 text-red-600',
|
|
22
|
-
register: 'bg-blue-50 text-blue-600',
|
|
23
|
-
password_change: 'bg-yellow-50 text-yellow-600',
|
|
24
|
-
password_reset: 'bg-purple-50 text-purple-600',
|
|
25
|
-
user_create: 'bg-green-50 text-green-600',
|
|
26
|
-
user_update: 'bg-blue-50 text-blue-600',
|
|
27
|
-
user_delete: 'bg-red-50 text-red-600',
|
|
28
|
-
user_lockout: 'bg-orange-50 text-orange-600',
|
|
29
|
-
};
|
|
3
|
+
import { AuditLogsPage } from '../../components/AuditLogsPage';
|
|
30
4
|
---
|
|
31
5
|
|
|
32
6
|
<AdminLayout title="Audit Logs">
|
|
33
|
-
<
|
|
34
|
-
<!-- Header -->
|
|
35
|
-
<div class="surface-tile p-6">
|
|
36
|
-
<h1 class="text-3xl font-black tracking-tighter text-[#0b1222]">Audit Logs</h1>
|
|
37
|
-
<p class="text-sm text-[#64748b] mt-1 font-medium">
|
|
38
|
-
Security audit trail
|
|
39
|
-
<span class="ml-2 text-[#0b1222] font-bold">· {totalLogs} entries</span>
|
|
40
|
-
</p>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<!-- Logs Table -->
|
|
44
|
-
<div class="surface-tile overflow-hidden">
|
|
45
|
-
{auditLogs.length === 0 ? (
|
|
46
|
-
<div class="px-8 py-16 text-center">
|
|
47
|
-
<div class="flex flex-col items-center gap-4">
|
|
48
|
-
<div class="w-16 h-16 rounded-2xl bg-gray-50 flex items-center justify-center">
|
|
49
|
-
<svg class="w-8 h-8 text-[#9ca3af]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
50
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="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"></path>
|
|
51
|
-
</svg>
|
|
52
|
-
</div>
|
|
53
|
-
<p class="font-bold text-[#0b1222] text-base">No audit logs yet</p>
|
|
54
|
-
<p class="text-sm text-[#64748b]">Logs will appear here as users interact with the system.</p>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
) : (
|
|
58
|
-
<table class="w-full text-left">
|
|
59
|
-
<thead>
|
|
60
|
-
<tr class="text-[#64748b] font-bold text-[10px] uppercase tracking-[0.3em] border-b border-gray-100">
|
|
61
|
-
<th class="px-8 py-6">Action</th>
|
|
62
|
-
<th class="px-6 py-6">User</th>
|
|
63
|
-
<th class="px-6 py-6">Resource</th>
|
|
64
|
-
<th class="px-6 py-6">IP Address</th>
|
|
65
|
-
<th class="px-6 py-6">Status</th>
|
|
66
|
-
<th class="px-6 py-6">Timestamp</th>
|
|
67
|
-
</tr>
|
|
68
|
-
</thead>
|
|
69
|
-
<tbody class="divide-y divide-gray-50">
|
|
70
|
-
{auditLogs.map((log) => (
|
|
71
|
-
<tr class="group hover:bg-gray-50/50 transition-colors">
|
|
72
|
-
<td class="px-8 py-4">
|
|
73
|
-
<span class={`inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold ${actionColors[log.action] || 'bg-gray-50 text-gray-600'}`}>
|
|
74
|
-
{log.action.replace(/_/g, ' ')}
|
|
75
|
-
</span>
|
|
76
|
-
</td>
|
|
77
|
-
<td class="px-6 py-4">
|
|
78
|
-
<div class="text-sm font-bold text-[#0b1222]">{log.userEmail || '—'}</div>
|
|
79
|
-
{log.role && <div class="text-xs text-[#64748b]">{log.role}</div>}
|
|
80
|
-
</td>
|
|
81
|
-
<td class="px-6 py-4 text-sm text-[#64748b]">{log.resource}</td>
|
|
82
|
-
<td class="px-6 py-4 text-sm text-[#64748b] font-mono">{log.ipAddress || '—'}</td>
|
|
83
|
-
<td class="px-6 py-4">
|
|
84
|
-
{log.success ? (
|
|
85
|
-
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-bold bg-green-50 text-green-600">
|
|
86
|
-
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
|
87
|
-
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
88
|
-
</svg>
|
|
89
|
-
Success
|
|
90
|
-
</span>
|
|
91
|
-
) : (
|
|
92
|
-
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-bold bg-red-50 text-red-600">
|
|
93
|
-
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
|
94
|
-
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
95
|
-
</svg>
|
|
96
|
-
Failed
|
|
97
|
-
</span>
|
|
98
|
-
)}
|
|
99
|
-
</td>
|
|
100
|
-
<td class="px-6 py-4 text-sm text-[#64748b]">
|
|
101
|
-
{log.timestamp ? new Date(log.timestamp).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'}
|
|
102
|
-
</td>
|
|
103
|
-
</tr>
|
|
104
|
-
))}
|
|
105
|
-
</tbody>
|
|
106
|
-
</table>
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
7
|
+
<AuditLogsPage client:only="react" />
|
|
110
8
|
</AdminLayout>
|
package/src/pages/login.astro
CHANGED
|
@@ -3,36 +3,36 @@ import AuthLayout from '../layouts/AuthLayout.astro';
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
<AuthLayout title="Sign In">
|
|
6
|
-
<div class="surface-tile p-8 w-full max-
|
|
6
|
+
<div class="surface-tile p-8 w-full" style="max-width: 420px;">
|
|
7
7
|
<div class="text-center mb-8">
|
|
8
|
-
<h1 class="text-2xl font-black tracking-tight text-[
|
|
9
|
-
<p class="text-sm text-[
|
|
8
|
+
<h1 class="text-2xl font-black tracking-tight text-[var(--kyro-text-primary)]">Welcome back</h1>
|
|
9
|
+
<p class="text-sm text-[var(--kyro-text-secondary)] mt-2">Sign in to your account</p>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
-
<form id="login-form" class="space-y-
|
|
12
|
+
<form id="login-form" class="space-y-5">
|
|
13
13
|
<div>
|
|
14
|
-
<label for="email" class="block text-sm font-bold text-[
|
|
14
|
+
<label for="email" class="block text-sm font-bold text-[var(--kyro-text-primary)] mb-2 text-left">Email</label>
|
|
15
15
|
<input type="email" id="email" name="email" required
|
|
16
|
-
class="w-full px-4 py-3 border border-
|
|
16
|
+
class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]"
|
|
17
17
|
placeholder="admin@example.com" />
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
20
|
<div>
|
|
21
|
-
<label for="password" class="block text-sm font-bold text-[
|
|
21
|
+
<label for="password" class="block text-sm font-bold text-[var(--kyro-text-primary)] mb-2">Password</label>
|
|
22
22
|
<input type="password" id="password" name="password" required
|
|
23
|
-
class="w-full px-4 py-3 border border-
|
|
23
|
+
class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]"
|
|
24
24
|
placeholder="••••••••" />
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
<div id="form-message" class="hidden p-3 rounded-xl text-sm font-bold"></div>
|
|
28
28
|
|
|
29
|
-
<button type="submit" class="w-full py-3 bg-[
|
|
29
|
+
<button type="submit" class="w-full py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-sm font-bold hover:opacity-90 transition-colors shadow-lg">
|
|
30
30
|
Sign In
|
|
31
31
|
</button>
|
|
32
32
|
</form>
|
|
33
33
|
|
|
34
|
-
<p class="text-center text-sm text-[
|
|
35
|
-
Don't have an account? <a href="/register" class="font-bold text-[
|
|
34
|
+
<p class="text-center text-sm text-[var(--kyro-text-secondary)] mt-6">
|
|
35
|
+
Don't have an account? <a href="/register" class="font-bold text-[var(--kyro-text-primary)] hover:underline">Register</a>
|
|
36
36
|
</p>
|
|
37
37
|
</div>
|
|
38
38
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AdminLayout from '../layouts/AdminLayout.astro';
|
|
3
|
+
import { MediaGallery } from '../components/MediaGallery';
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<AdminLayout title="Media Library">
|
|
7
|
+
<div class="flex-1 overflow-hidden">
|
|
8
|
+
<MediaGallery client:load />
|
|
9
|
+
</div>
|
|
10
|
+
</AdminLayout>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AdminLayout from '../../../layouts/AdminLayout.astro';
|
|
3
|
+
import {
|
|
4
|
+
renderRichText,
|
|
5
|
+
richTextStyles,
|
|
6
|
+
} from "../../../../../src/fields/richtext.js";
|
|
7
|
+
import { collections } from '@/lib/config';
|
|
8
|
+
|
|
9
|
+
const { collection, id } = Astro.params;
|
|
10
|
+
|
|
11
|
+
if (!collection || !collections[collection]) {
|
|
12
|
+
return Astro.redirect('/');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const config = collections[collection];
|
|
16
|
+
|
|
17
|
+
let doc = null;
|
|
18
|
+
let error = null;
|
|
19
|
+
|
|
20
|
+
if (id) {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(`${Astro.url.origin}/api/${collection}/${id}`, {
|
|
23
|
+
headers: Astro.request.headers,
|
|
24
|
+
credentials: 'include'
|
|
25
|
+
});
|
|
26
|
+
if (response.ok) {
|
|
27
|
+
const result = await response.json();
|
|
28
|
+
doc = result.data || null;
|
|
29
|
+
} else {
|
|
30
|
+
error = `Failed to load document: ${response.status}`;
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
error = 'Failed to fetch document';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const title = doc ? `Preview: ${doc.title || doc.name || doc.slug || id}` : 'Preview';
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<AdminLayout title={title}>
|
|
41
|
+
<Fragment set:html={`<style>${richTextStyles}</style>`} />
|
|
42
|
+
<div class="flex-1 overflow-y-auto p-8 space-y-6">
|
|
43
|
+
<div class="surface-tile p-6 flex items-center justify-between">
|
|
44
|
+
<div class="flex items-center gap-4">
|
|
45
|
+
<a
|
|
46
|
+
href={`/${collection}/${id}`}
|
|
47
|
+
class="inline-flex items-center justify-center w-10 h-10 rounded-xl border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
48
|
+
>
|
|
49
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
50
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7"></path>
|
|
51
|
+
</svg>
|
|
52
|
+
</a>
|
|
53
|
+
<div>
|
|
54
|
+
<h1 class="text-xl font-black tracking-tighter text-[var(--kyro-text-primary)]">Preview Mode</h1>
|
|
55
|
+
<p class="text-sm text-[var(--kyro-text-secondary)] mt-0.5">Showing draft content</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="flex items-center gap-3">
|
|
59
|
+
<span class="px-3 py-1.5 bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] text-xs font-bold rounded-lg border border-[var(--kyro-border)]">
|
|
60
|
+
{doc?.status || 'draft'}
|
|
61
|
+
</span>
|
|
62
|
+
<a
|
|
63
|
+
href={`/${collection}/${id}`}
|
|
64
|
+
class="px-5 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-sm hover:opacity-90 transition-colors"
|
|
65
|
+
>
|
|
66
|
+
Edit
|
|
67
|
+
</a>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{error && (
|
|
72
|
+
<div class="surface-tile p-6 border border-red-500/30 bg-red-500/10">
|
|
73
|
+
<p class="text-red-400">{error}</p>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{doc && (
|
|
78
|
+
<div class="space-y-6">
|
|
79
|
+
{config.fields.map((field: any) => {
|
|
80
|
+
const value = doc[field.name];
|
|
81
|
+
if (value === undefined || value === null) return null;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div class="surface-tile p-6">
|
|
85
|
+
<h3 class="text-xs font-bold text-[var(--kyro-text-muted)] uppercase tracking-[0.15em] mb-3">
|
|
86
|
+
{field.label || field.name}
|
|
87
|
+
</h3>
|
|
88
|
+
<div class="text-sm text-[var(--kyro-text-primary)]">
|
|
89
|
+
{field.type === 'text' && (
|
|
90
|
+
<p class="whitespace-pre-wrap">{value}</p>
|
|
91
|
+
)}
|
|
92
|
+
{field.type === 'textarea' && (
|
|
93
|
+
<p class="whitespace-pre-wrap">{value}</p>
|
|
94
|
+
)}
|
|
95
|
+
{field.type === 'richtext' && (
|
|
96
|
+
<div set:html={renderRichText(value)} />
|
|
97
|
+
)}
|
|
98
|
+
{field.type === 'markdown' && (
|
|
99
|
+
<div class="prose prose-sm max-w-none">
|
|
100
|
+
<pre class="whitespace-pre-wrap bg-transparent p-0">{value}</pre>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
{field.type === 'number' && (
|
|
104
|
+
<p>{value}</p>
|
|
105
|
+
)}
|
|
106
|
+
{field.type === 'boolean' && (
|
|
107
|
+
<span class={value ? 'text-green-400' : 'text-red-400'}>{value ? 'Yes' : 'No'}</span>
|
|
108
|
+
)}
|
|
109
|
+
{field.type === 'select' && (
|
|
110
|
+
<span class="px-2 py-1 bg-[var(--kyro-surface-accent)] rounded text-xs">{value}</span>
|
|
111
|
+
)}
|
|
112
|
+
{field.type === 'date' && (
|
|
113
|
+
<p>{new Date(value).toLocaleString()}</p>
|
|
114
|
+
)}
|
|
115
|
+
{field.type === 'upload' && value.url && (
|
|
116
|
+
<div>
|
|
117
|
+
{value.url.match(/\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i) ? (
|
|
118
|
+
<img src={value.url} alt={value.filename || 'Image'} class="max-w-md rounded-lg border border-[var(--kyro-border)]" />
|
|
119
|
+
) : (
|
|
120
|
+
<a href={value.url} class="text-blue-400 hover:underline">{value.filename || 'Download'}</a>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
{field.type === 'gallery' && Array.isArray(value) && (
|
|
125
|
+
<div class="grid grid-cols-4 gap-3">
|
|
126
|
+
{value.map((item: any, idx: number) => (
|
|
127
|
+
item?.url ? (
|
|
128
|
+
item.url.match(/\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i) ? (
|
|
129
|
+
<img key={idx} src={item.url} alt={item.filename || `Image ${idx + 1}`} class="w-full h-24 object-cover rounded-lg border border-[var(--kyro-border)]" />
|
|
130
|
+
) : (
|
|
131
|
+
<div key={idx} class="h-24 flex items-center justify-center bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)] text-xs">
|
|
132
|
+
{item.filename || 'File'}
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
) : null
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
{field.type === 'array' && Array.isArray(value) && (
|
|
140
|
+
<div class="space-y-2">
|
|
141
|
+
{value.map((item: any, idx: number) => (
|
|
142
|
+
<div key={idx} class="p-3 bg-[var(--kyro-surface-accent)] rounded border border-[var(--kyro-border)] text-xs">
|
|
143
|
+
{Object.entries(item).filter(([k, v]) => v !== undefined).map(([k, v]) => (
|
|
144
|
+
<div key={k} class="flex gap-2">
|
|
145
|
+
<span class="text-[var(--kyro-text-muted)]">{k}:</span>
|
|
146
|
+
<span>{typeof v === 'object' ? JSON.stringify(v) : String(v)}</span>
|
|
147
|
+
</div>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
{field.type === 'relationship' && (
|
|
154
|
+
<p class="text-[var(--kyro-text-secondary)]">{value.title || value.name || value.slug || JSON.stringify(value)}</p>
|
|
155
|
+
)}
|
|
156
|
+
{field.type === 'json' && (
|
|
157
|
+
<pre class="text-xs bg-[var(--kyro-surface-accent)] p-3 rounded overflow-x-auto">{JSON.stringify(value, null, 2)}</pre>
|
|
158
|
+
)}
|
|
159
|
+
{field.type === 'code' && (
|
|
160
|
+
<pre class="text-xs bg-[var(--kyro-surface-accent)] p-3 rounded overflow-x-auto">{value}</pre>
|
|
161
|
+
)}
|
|
162
|
+
{(!field.type || ['text', 'string'].includes(field.type)) && (
|
|
163
|
+
<p>{value}</p>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{!doc && !error && (
|
|
173
|
+
<div class="surface-tile p-6">
|
|
174
|
+
<p class="text-[var(--kyro-text-muted)]">Document not found</p>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</AdminLayout>
|
package/src/pages/register.astro
CHANGED
|
@@ -3,43 +3,43 @@ import AuthLayout from '../layouts/AuthLayout.astro';
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
<AuthLayout title="Create Account">
|
|
6
|
-
<div class="surface-tile p-8 w-full max-
|
|
6
|
+
<div class="surface-tile p-8 w-full" style="max-width: 420px;">
|
|
7
7
|
<div class="text-center mb-8">
|
|
8
|
-
<h1 class="text-2xl font-black tracking-tight text-[
|
|
9
|
-
<p class="text-sm text-[
|
|
8
|
+
<h1 class="text-2xl font-black tracking-tight text-[var(--kyro-text-primary)]">Create your account</h1>
|
|
9
|
+
<p class="text-sm text-[var(--kyro-text-secondary)] mt-2">Get started with Kyro CMS</p>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
-
<form id="register-form" class="space-y-
|
|
12
|
+
<form id="register-form" class="space-y-5">
|
|
13
13
|
<div>
|
|
14
|
-
<label for="email" class="block text-sm font-bold text-[
|
|
14
|
+
<label for="email" class="block text-sm font-bold text-[var(--kyro-text-primary)] mb-2">Email</label>
|
|
15
15
|
<input type="email" id="email" name="email" required
|
|
16
|
-
class="w-full px-4 py-3 border border-
|
|
16
|
+
class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]"
|
|
17
17
|
placeholder="admin@example.com" />
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
20
|
<div>
|
|
21
|
-
<label for="password" class="block text-sm font-bold text-[
|
|
21
|
+
<label for="password" class="block text-sm font-bold text-[var(--kyro-text-primary)] mb-2">Password</label>
|
|
22
22
|
<input type="password" id="password" name="password" required minlength="8"
|
|
23
|
-
class="w-full px-4 py-3 border border-
|
|
23
|
+
class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]"
|
|
24
24
|
placeholder="Minimum 8 characters" />
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
<div>
|
|
28
|
-
<label for="confirmPassword" class="block text-sm font-bold text-[
|
|
28
|
+
<label for="confirmPassword" class="block text-sm font-bold text-[var(--kyro-text-primary)] mb-2">Confirm Password</label>
|
|
29
29
|
<input type="password" id="confirmPassword" name="confirmPassword" required minlength="8"
|
|
30
|
-
class="w-full px-4 py-3 border border-
|
|
30
|
+
class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]"
|
|
31
31
|
placeholder="Confirm your password" />
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
34
|
<div id="form-message" class="hidden p-3 rounded-xl text-sm font-bold"></div>
|
|
35
35
|
|
|
36
|
-
<button type="submit" class="w-full py-3 bg-[
|
|
36
|
+
<button type="submit" class="w-full py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-sm font-bold hover:opacity-90 transition-colors shadow-lg">
|
|
37
37
|
Create Account
|
|
38
38
|
</button>
|
|
39
39
|
</form>
|
|
40
40
|
|
|
41
|
-
<p class="text-center text-sm text-[
|
|
42
|
-
Already have an account? <a href="/login" class="font-bold text-[
|
|
41
|
+
<p class="text-center text-sm text-[var(--kyro-text-secondary)] mt-6">
|
|
42
|
+
Already have an account? <a href="/login" class="font-bold text-[var(--kyro-text-primary)] hover:underline">Sign in</a>
|
|
43
43
|
</p>
|
|
44
44
|
</div>
|
|
45
45
|
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import AdminLayout from '../../layouts/AdminLayout.astro';
|
|
3
3
|
|
|
4
4
|
const defaultRoles = [
|
|
5
|
-
{ name: 'super_admin', level: 100, inherits: ['admin'], description: 'Full system access across all tenants', color: 'bg-red-
|
|
6
|
-
{ name: 'admin', level: 90, inherits: ['editor'], description: 'Full tenant access with all content permissions', color: 'bg-purple-
|
|
7
|
-
{ name: 'editor', level: 70, inherits: ['author'], description: 'Edit and publish all content', color: 'bg-blue-
|
|
8
|
-
{ name: 'author', level: 50, inherits: ['customer'], description: 'Create and edit own content', color: 'bg-green-
|
|
9
|
-
{ name: 'customer', level: 30, inherits: [], description: 'Access own data and make purchases', color: 'bg-
|
|
10
|
-
{ name: 'guest', level: 10, inherits: [], description: 'Public read-only access', color: 'bg-yellow-
|
|
5
|
+
{ name: 'super_admin', level: 100, inherits: ['admin'], description: 'Full system access across all tenants', color: 'bg-red-500/10 text-red-500 border-red-500/20' },
|
|
6
|
+
{ name: 'admin', level: 90, inherits: ['editor'], description: 'Full tenant access with all content permissions', color: 'bg-purple-500/10 text-purple-500 border-purple-500/20' },
|
|
7
|
+
{ name: 'editor', level: 70, inherits: ['author'], description: 'Edit and publish all content', color: 'bg-blue-500/10 text-blue-500 border-blue-500/20' },
|
|
8
|
+
{ name: 'author', level: 50, inherits: ['customer'], description: 'Create and edit own content', color: 'bg-green-500/10 text-green-500 border-green-500/20' },
|
|
9
|
+
{ name: 'customer', level: 30, inherits: [], description: 'Access own data and make purchases', color: 'bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] border-[var(--kyro-border)]' },
|
|
10
|
+
{ name: 'guest', level: 10, inherits: [], description: 'Public read-only access', color: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20' },
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
const permissions: Record<string, string[]> = {
|
|
@@ -24,8 +24,8 @@ const permissions: Record<string, string[]> = {
|
|
|
24
24
|
<div class="flex-1 overflow-y-auto p-8 pr-12 space-y-8">
|
|
25
25
|
<!-- Header -->
|
|
26
26
|
<div class="surface-tile p-6">
|
|
27
|
-
<h1 class="text-3xl font-black tracking-tighter text-[
|
|
28
|
-
<p class="text-sm text-[
|
|
27
|
+
<h1 class="text-3xl font-black tracking-tighter text-[var(--kyro-text-primary)]">Roles & Permissions</h1>
|
|
28
|
+
<p class="text-sm text-[var(--kyro-text-secondary)] mt-1 font-medium italic">
|
|
29
29
|
Role-based access control with hierarchical permissions
|
|
30
30
|
</p>
|
|
31
31
|
</div>
|
|
@@ -35,27 +35,27 @@ const permissions: Record<string, string[]> = {
|
|
|
35
35
|
{defaultRoles.map((role) => (
|
|
36
36
|
<div class={`surface-tile p-6 border-2 ${role.color.split(' ').slice(2).join(' ')} rounded-2xl`}>
|
|
37
37
|
<div class="flex items-center justify-between mb-4">
|
|
38
|
-
<h3 class="text-xl font-black text-[
|
|
38
|
+
<h3 class="text-xl font-black text-[var(--kyro-text-primary)] tracking-tighter">{role.name}</h3>
|
|
39
39
|
<span class={`px-3 py-1 rounded-lg text-xs font-bold ${role.color.split(' ').slice(0, 2).join(' ')}`}>
|
|
40
40
|
Level {role.level}
|
|
41
41
|
</span>
|
|
42
42
|
</div>
|
|
43
|
-
<p class="text-sm text-[
|
|
43
|
+
<p class="text-sm text-[var(--kyro-text-secondary)] mb-4">{role.description}</p>
|
|
44
44
|
{role.inherits.length > 0 && (
|
|
45
45
|
<div class="mb-4">
|
|
46
|
-
<span class="text-xs font-bold text-[
|
|
46
|
+
<span class="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-[0.1em] opacity-50">Inherits:</span>
|
|
47
47
|
<div class="flex gap-2 mt-2">
|
|
48
48
|
{role.inherits.map((parent) => (
|
|
49
|
-
<span class="px-2 py-1 bg-
|
|
49
|
+
<span class="px-2 py-1 bg-[var(--kyro-surface-accent)] rounded text-xs font-bold text-[var(--kyro-text-secondary)]">{parent}</span>
|
|
50
50
|
))}
|
|
51
51
|
</div>
|
|
52
52
|
</div>
|
|
53
53
|
)}
|
|
54
54
|
<div>
|
|
55
|
-
<span class="text-xs font-bold text-[
|
|
55
|
+
<span class="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-[0.1em] opacity-50">Permissions:</span>
|
|
56
56
|
<div class="flex flex-wrap gap-1.5 mt-2">
|
|
57
57
|
{(permissions[role.name] || []).map((perm) => (
|
|
58
|
-
<span class="px-2 py-0.5 bg-
|
|
58
|
+
<span class="px-2 py-0.5 bg-[var(--kyro-surface-accent)] rounded text-[10px] font-bold text-[var(--kyro-text-primary)]">{perm}</span>
|
|
59
59
|
))}
|
|
60
60
|
</div>
|
|
61
61
|
</div>
|
|
@@ -65,19 +65,19 @@ const permissions: Record<string, string[]> = {
|
|
|
65
65
|
|
|
66
66
|
<!-- Permission Matrix -->
|
|
67
67
|
<div class="surface-tile overflow-hidden">
|
|
68
|
-
<div class="p-6 border-b border-
|
|
69
|
-
<h2 class="text-xl font-black text-[
|
|
68
|
+
<div class="p-6 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]">
|
|
69
|
+
<h2 class="text-xl font-black text-[var(--kyro-text-primary)] tracking-tighter">Permission Matrix</h2>
|
|
70
70
|
</div>
|
|
71
71
|
<table class="w-full text-left">
|
|
72
72
|
<thead>
|
|
73
|
-
<tr class="text-[
|
|
73
|
+
<tr class="text-[var(--kyro-text-secondary)] font-bold text-[10px] uppercase tracking-[0.3em] border-b border-[var(--kyro-border)]">
|
|
74
74
|
<th class="px-6 py-4">Resource</th>
|
|
75
75
|
{defaultRoles.map((role) => (
|
|
76
76
|
<th class="px-4 py-4 text-center">{role.name}</th>
|
|
77
77
|
))}
|
|
78
78
|
</tr>
|
|
79
79
|
</thead>
|
|
80
|
-
<tbody class="divide-y divide-
|
|
80
|
+
<tbody class="divide-y divide-[var(--kyro-border)]">
|
|
81
81
|
{[
|
|
82
82
|
{ resource: 'Users', actions: ['create', 'read', 'update', 'delete'] },
|
|
83
83
|
{ resource: 'Roles', actions: ['create', 'read', 'update', 'delete'] },
|
|
@@ -86,8 +86,8 @@ const permissions: Record<string, string[]> = {
|
|
|
86
86
|
{ resource: 'Settings', actions: ['read', 'update'] },
|
|
87
87
|
{ resource: 'Audit Logs', actions: ['read'] },
|
|
88
88
|
].map((row) => (
|
|
89
|
-
<tr class="hover:bg-
|
|
90
|
-
<td class="px-6 py-4 font-bold text-[
|
|
89
|
+
<tr class="hover:bg-[var(--kyro-surface-accent)] transition-colors">
|
|
90
|
+
<td class="px-6 py-4 font-bold text-[var(--kyro-text-primary)]">{row.resource}</td>
|
|
91
91
|
{defaultRoles.map((role) => {
|
|
92
92
|
const hasAll = permissions[role.name]?.includes('*');
|
|
93
93
|
const hasResource = permissions[role.name]?.some(p => p.startsWith(row.resource.toLowerCase() + ':') || p === '*');
|
|
@@ -98,7 +98,7 @@ const permissions: Record<string, string[]> = {
|
|
|
98
98
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
99
99
|
</svg>
|
|
100
100
|
) : (
|
|
101
|
-
<svg class="w-5 h-5 text-
|
|
101
|
+
<svg class="w-5 h-5 text-[var(--kyro-border)] mx-auto" fill="currentColor" viewBox="0 0 20 20">
|
|
102
102
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
103
103
|
</svg>
|
|
104
104
|
)}
|