@kyro-cms/admin 0.1.5 → 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 +52 -5
- 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 +50 -0
- 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 +116 -28
- 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 +286 -0
- 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 +50 -20
- 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 +82 -0
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +102 -0
- 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
- package/src/pages/index.astro +0 -225
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import type { AuthAdapter, AuthUser, Session, UserRole } from "@kyro-cms/core";
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
|
|
8
|
+
const DB_DIR = path.join(process.cwd(), "data");
|
|
9
|
+
const DB_PATH = path.join(DB_DIR, "kyro.db");
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(DB_DIR)) {
|
|
12
|
+
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const db = new Database(DB_PATH);
|
|
16
|
+
db.pragma("journal_mode = WAL");
|
|
17
|
+
|
|
18
|
+
db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
email TEXT UNIQUE NOT NULL,
|
|
22
|
+
password_hash TEXT NOT NULL,
|
|
23
|
+
name TEXT,
|
|
24
|
+
role TEXT DEFAULT 'customer',
|
|
25
|
+
created_at TEXT NOT NULL,
|
|
26
|
+
updated_at TEXT NOT NULL
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
30
|
+
token TEXT PRIMARY KEY,
|
|
31
|
+
user_id TEXT NOT NULL,
|
|
32
|
+
expires_at TEXT NOT NULL,
|
|
33
|
+
created_at TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS roles (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
name TEXT UNIQUE NOT NULL,
|
|
39
|
+
level INTEGER DEFAULT 50,
|
|
40
|
+
inherits TEXT DEFAULT '[]',
|
|
41
|
+
permissions TEXT DEFAULT '[]',
|
|
42
|
+
description TEXT,
|
|
43
|
+
created_at TEXT NOT NULL
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
user_id TEXT,
|
|
49
|
+
action TEXT NOT NULL,
|
|
50
|
+
entity_type TEXT,
|
|
51
|
+
entity_id TEXT,
|
|
52
|
+
metadata TEXT DEFAULT '{}',
|
|
53
|
+
ip_address TEXT,
|
|
54
|
+
user_agent TEXT,
|
|
55
|
+
created_at TEXT NOT NULL
|
|
56
|
+
);
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
export interface SQLiteAuthAdapterOptions {
|
|
60
|
+
tokenExpiration?: number;
|
|
61
|
+
refreshTokenExpiration?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DEFAULT_TOKEN_EXPIRATION = 86400;
|
|
65
|
+
const DEFAULT_REFRESH_EXPIRATION = 604800;
|
|
66
|
+
|
|
67
|
+
export class SQLiteAuthAdapter implements AuthAdapter {
|
|
68
|
+
private tokenExpiration: number;
|
|
69
|
+
private refreshExpiration: number;
|
|
70
|
+
|
|
71
|
+
constructor(options: SQLiteAuthAdapterOptions = {}) {
|
|
72
|
+
this.tokenExpiration = options.tokenExpiration || DEFAULT_TOKEN_EXPIRATION;
|
|
73
|
+
this.refreshExpiration =
|
|
74
|
+
options.refreshTokenExpiration || DEFAULT_REFRESH_EXPIRATION;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private getTimestamp(): string {
|
|
78
|
+
return new Date().toISOString();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private generateId(): string {
|
|
82
|
+
return randomBytes(16).toString("hex");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private getDb(): Database.Database {
|
|
86
|
+
return db;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async findUserById(id: string): Promise<AuthUser | null> {
|
|
90
|
+
const user = db.prepare("SELECT * FROM users WHERE id = ?").get(id) as any;
|
|
91
|
+
if (!user) return null;
|
|
92
|
+
return {
|
|
93
|
+
id: user.id,
|
|
94
|
+
email: user.email,
|
|
95
|
+
name: user.name,
|
|
96
|
+
role: user.role as UserRole,
|
|
97
|
+
createdAt: user.created_at,
|
|
98
|
+
updatedAt: user.updated_at,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async findUserByEmail(email: string): Promise<AuthUser | null> {
|
|
103
|
+
const user = db
|
|
104
|
+
.prepare("SELECT * FROM users WHERE email = ?")
|
|
105
|
+
.get(email) as any;
|
|
106
|
+
if (!user) return null;
|
|
107
|
+
return {
|
|
108
|
+
id: user.id,
|
|
109
|
+
email: user.email,
|
|
110
|
+
name: user.name,
|
|
111
|
+
role: user.role as UserRole,
|
|
112
|
+
createdAt: user.created_at,
|
|
113
|
+
updatedAt: user.updated_at,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async createUser(
|
|
118
|
+
userData: Partial<AuthUser> & { password: string },
|
|
119
|
+
): Promise<AuthUser> {
|
|
120
|
+
const now = this.getTimestamp();
|
|
121
|
+
const id = this.generateId();
|
|
122
|
+
const passwordHash = await bcrypt.hash(userData.password, 10);
|
|
123
|
+
|
|
124
|
+
db.prepare(
|
|
125
|
+
"INSERT INTO users (id, email, password_hash, name, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
126
|
+
).run(
|
|
127
|
+
id,
|
|
128
|
+
userData.email,
|
|
129
|
+
passwordHash,
|
|
130
|
+
userData.name || null,
|
|
131
|
+
userData.role || "customer",
|
|
132
|
+
now,
|
|
133
|
+
now,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
id,
|
|
138
|
+
email: userData.email,
|
|
139
|
+
name: userData.name,
|
|
140
|
+
role: userData.role || "customer",
|
|
141
|
+
createdAt: now,
|
|
142
|
+
updatedAt: now,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async updateUser(
|
|
147
|
+
id: string,
|
|
148
|
+
data: Partial<AuthUser> & { password?: string },
|
|
149
|
+
): Promise<AuthUser | null> {
|
|
150
|
+
const existing = await this.findUserById(id);
|
|
151
|
+
if (!existing) return null;
|
|
152
|
+
|
|
153
|
+
const now = this.getTimestamp();
|
|
154
|
+
const updates: string[] = [];
|
|
155
|
+
const values: any[] = [];
|
|
156
|
+
|
|
157
|
+
if (data.email !== undefined) {
|
|
158
|
+
updates.push("email = ?");
|
|
159
|
+
values.push(data.email);
|
|
160
|
+
}
|
|
161
|
+
if (data.name !== undefined) {
|
|
162
|
+
updates.push("name = ?");
|
|
163
|
+
values.push(data.name);
|
|
164
|
+
}
|
|
165
|
+
if (data.role !== undefined) {
|
|
166
|
+
updates.push("role = ?");
|
|
167
|
+
values.push(data.role);
|
|
168
|
+
}
|
|
169
|
+
if (data.password !== undefined) {
|
|
170
|
+
updates.push("password_hash = ?");
|
|
171
|
+
values.push(await bcrypt.hash(data.password, 10));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
updates.push("updated_at = ?");
|
|
175
|
+
values.push(now);
|
|
176
|
+
values.push(id);
|
|
177
|
+
|
|
178
|
+
db.prepare(`UPDATE users SET ${updates.join(", ")} WHERE id = ?`).run(
|
|
179
|
+
...values,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return this.findUserById(id);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async deleteUser(id: string): Promise<boolean> {
|
|
186
|
+
const result = db.prepare("DELETE FROM users WHERE id = ?").run(id);
|
|
187
|
+
if (result.changes > 0) {
|
|
188
|
+
db.prepare("DELETE FROM sessions WHERE user_id = ?").run(id);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async verifyPassword(
|
|
195
|
+
email: string,
|
|
196
|
+
password: string,
|
|
197
|
+
): Promise<AuthUser | null> {
|
|
198
|
+
const user = db
|
|
199
|
+
.prepare("SELECT * FROM users WHERE email = ?")
|
|
200
|
+
.get(email) as any;
|
|
201
|
+
if (!user) return null;
|
|
202
|
+
|
|
203
|
+
const valid = await bcrypt.compare(password, user.password_hash);
|
|
204
|
+
if (!valid) return null;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
id: user.id,
|
|
208
|
+
email: user.email,
|
|
209
|
+
name: user.name,
|
|
210
|
+
role: user.role as UserRole,
|
|
211
|
+
createdAt: user.created_at,
|
|
212
|
+
updatedAt: user.updated_at,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async createSession(userId: string): Promise<Session> {
|
|
217
|
+
const now = new Date();
|
|
218
|
+
const expiresAt = new Date(now.getTime() + this.tokenExpiration * 1000);
|
|
219
|
+
const refreshExpiresAt = new Date(
|
|
220
|
+
now.getTime() + this.refreshExpiration * 1000,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const token = randomBytes(32).toString("hex");
|
|
224
|
+
const refreshToken = randomBytes(32).toString("hex");
|
|
225
|
+
|
|
226
|
+
db.prepare(
|
|
227
|
+
"INSERT INTO sessions (token, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)",
|
|
228
|
+
).run(token, userId, expiresAt.toISOString(), now.toISOString());
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
token,
|
|
232
|
+
refreshToken,
|
|
233
|
+
expiresAt: expiresAt.toISOString(),
|
|
234
|
+
userId,
|
|
235
|
+
createdAt: now.toISOString(),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async validateSession(token: string): Promise<Session | null> {
|
|
240
|
+
const now = new Date().toISOString();
|
|
241
|
+
const session = db
|
|
242
|
+
.prepare("SELECT * FROM sessions WHERE token = ? AND expires_at > ?")
|
|
243
|
+
.get(token, now) as any;
|
|
244
|
+
|
|
245
|
+
if (!session) return null;
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
token: session.token,
|
|
249
|
+
userId: session.user_id,
|
|
250
|
+
expiresAt: session.expires_at,
|
|
251
|
+
createdAt: session.created_at,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async refreshSession(token: string): Promise<Session | null> {
|
|
256
|
+
const session = await this.validateSession(token);
|
|
257
|
+
if (!session) return null;
|
|
258
|
+
|
|
259
|
+
return this.createSession(session.userId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async revokeSession(token: string): Promise<void> {
|
|
263
|
+
db.prepare("DELETE FROM sessions WHERE token = ?").run(token);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async revokeAllUserSessions(userId: string): Promise<void> {
|
|
267
|
+
db.prepare("DELETE FROM sessions WHERE user_id = ?").run(userId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async findUserRoles(): Promise<UserRole[]> {
|
|
271
|
+
const roles = db
|
|
272
|
+
.prepare("SELECT * FROM roles ORDER BY level DESC")
|
|
273
|
+
.all() as any[];
|
|
274
|
+
return roles.map((r) => ({
|
|
275
|
+
id: r.id,
|
|
276
|
+
name: r.name,
|
|
277
|
+
level: r.level,
|
|
278
|
+
inherits: JSON.parse(r.inherits || "[]"),
|
|
279
|
+
permissions: JSON.parse(r.permissions || "[]"),
|
|
280
|
+
description: r.description,
|
|
281
|
+
createdAt: r.created_at,
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async createRole(role: Partial<UserRole>): Promise<UserRole> {
|
|
286
|
+
const now = this.getTimestamp();
|
|
287
|
+
const id = role.id || this.generateId();
|
|
288
|
+
|
|
289
|
+
db.prepare(
|
|
290
|
+
"INSERT INTO roles (id, name, level, inherits, permissions, description, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
291
|
+
).run(
|
|
292
|
+
id,
|
|
293
|
+
role.name,
|
|
294
|
+
role.level || 50,
|
|
295
|
+
JSON.stringify(role.inherits || []),
|
|
296
|
+
JSON.stringify(role.permissions || []),
|
|
297
|
+
role.description || null,
|
|
298
|
+
now,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
id,
|
|
303
|
+
name: role.name!,
|
|
304
|
+
level: role.level || 50,
|
|
305
|
+
inherits: role.inherits || [],
|
|
306
|
+
permissions: role.permissions || [],
|
|
307
|
+
description: role.description,
|
|
308
|
+
createdAt: now,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async findUserByIdWithPassword(
|
|
313
|
+
id: string,
|
|
314
|
+
): Promise<{ id: string; email: string; password_hash: string } | null> {
|
|
315
|
+
return db
|
|
316
|
+
.prepare("SELECT id, email, password_hash FROM users WHERE id = ?")
|
|
317
|
+
.get(id) as any;
|
|
318
|
+
}
|
|
319
|
+
}
|
package/src/lib/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core";
|
|
2
|
-
import { blogCollections } from "../../../src/
|
|
3
|
-
import { ecommerceCollections } from "../../../src/
|
|
4
|
-
import { minimalCollections } from "../../../src/
|
|
5
|
-
import { kitchenSinkCollections } from "../../../src/
|
|
2
|
+
import { blogCollections } from "../../../src/templates/blog";
|
|
3
|
+
import { ecommerceCollections } from "../../../src/templates/ecommerce";
|
|
4
|
+
import { minimalCollections } from "../../../src/templates/minimal";
|
|
5
|
+
import { kitchenSinkCollections } from "../../../src/templates/kitchen-sink";
|
|
6
6
|
import { mediaCollections } from "../../../src/templates/media";
|
|
7
7
|
import {
|
|
8
8
|
allSettingsGlobals,
|
|
@@ -10,8 +10,14 @@ import {
|
|
|
10
10
|
ecommerceSettingsGlobals,
|
|
11
11
|
} from "../../../src/templates/settings";
|
|
12
12
|
import { authCollections } from "../collections/auth";
|
|
13
|
+
import { portfolioCollections } from "../collections/portfolio";
|
|
13
14
|
|
|
14
|
-
export type AdminTemplate =
|
|
15
|
+
export type AdminTemplate =
|
|
16
|
+
| "minimal"
|
|
17
|
+
| "blog"
|
|
18
|
+
| "ecommerce"
|
|
19
|
+
| "kitchen-sink"
|
|
20
|
+
| "portfolio";
|
|
15
21
|
|
|
16
22
|
export function getAdminConfig(template: AdminTemplate = "blog") {
|
|
17
23
|
const collections: CollectionConfig[] = [];
|
|
@@ -42,6 +48,11 @@ export function getAdminConfig(template: AdminTemplate = "blog") {
|
|
|
42
48
|
);
|
|
43
49
|
globals.push(...allSettingsGlobals);
|
|
44
50
|
break;
|
|
51
|
+
case "portfolio":
|
|
52
|
+
collections.push(...Object.values(blogCollections));
|
|
53
|
+
collections.push(...Object.values(portfolioCollections));
|
|
54
|
+
globals.push(...coreSettingsGlobals);
|
|
55
|
+
break;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
const collectionsMap = collections.reduce(
|
|
@@ -63,6 +74,11 @@ export function getAdminConfig(template: AdminTemplate = "blog") {
|
|
|
63
74
|
return { collections: collectionsMap, globals: globalsMap };
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
export const adminConfig = getAdminConfig("
|
|
77
|
+
export const adminConfig = getAdminConfig("minimal");
|
|
67
78
|
export const collections = adminConfig.collections;
|
|
68
79
|
export const globals = adminConfig.globals;
|
|
80
|
+
|
|
81
|
+
export const authCollectionSlugs = ["users", "roles", "audit_logs"];
|
|
82
|
+
export const nonAuthCollections = Object.values(collections).filter(
|
|
83
|
+
(c) => !authCollectionSlugs.includes(c.slug),
|
|
84
|
+
);
|
package/src/lib/dataStore.ts
CHANGED
|
@@ -1,111 +1,169 @@
|
|
|
1
1
|
import type { CollectionConfig } from "@kyro-cms/core";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import {
|
|
4
|
+
initializeDatabase,
|
|
5
|
+
getDatabaseAdapter,
|
|
6
|
+
isDatabaseInitialized,
|
|
7
|
+
} from "./db";
|
|
2
8
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
class DataStoreWrapper {
|
|
10
|
+
private async getStore() {
|
|
11
|
+
if (!isDatabaseInitialized()) {
|
|
12
|
+
await initializeDatabase();
|
|
13
|
+
}
|
|
14
|
+
return getDatabaseAdapter();
|
|
15
|
+
}
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
private data: Map<string, Document[]> = new Map();
|
|
10
|
-
private metadata: Map<string, { createdAt: string; updatedAt: string }> =
|
|
11
|
-
new Map();
|
|
12
|
-
private idCounters: Map<string, number> = new Map();
|
|
17
|
+
private readonly VERSIONS_COLLECTION = "_versions";
|
|
13
18
|
|
|
14
19
|
initialize(collections: Record<string, CollectionConfig>) {
|
|
15
|
-
|
|
16
|
-
if (!this.data.has(slug)) {
|
|
17
|
-
this.data.set(slug, []);
|
|
18
|
-
this.idCounters.set(slug, 1);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
20
|
+
initializeDatabase(collections); // Sync wrapper - adapter handles initialization
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
this.idCounters.set(slug, counter + 1);
|
|
26
|
-
return `${slug}-${counter}`;
|
|
23
|
+
private async getTimestamp(): Promise<string> {
|
|
24
|
+
return new Date().toISOString();
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
private
|
|
30
|
-
return
|
|
27
|
+
private generateId(): string {
|
|
28
|
+
return randomUUID();
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
find<T = any>(
|
|
31
|
+
async find<T = any>(
|
|
34
32
|
slug: string,
|
|
35
33
|
options: { page?: number; limit?: number } = {},
|
|
36
|
-
): {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
-
docs: docs.slice(start, end) as T[],
|
|
45
|
-
totalDocs: docs.length,
|
|
46
|
-
totalPages: Math.ceil(docs.length / limit),
|
|
47
|
-
page,
|
|
48
|
-
};
|
|
34
|
+
): Promise<{
|
|
35
|
+
docs: T[];
|
|
36
|
+
totalDocs: number;
|
|
37
|
+
totalPages: number;
|
|
38
|
+
page: number;
|
|
39
|
+
}> {
|
|
40
|
+
const store = await this.getStore();
|
|
41
|
+
return store.find(slug, options);
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
findById<T = any>(slug: string, id: string): T | null {
|
|
52
|
-
const
|
|
53
|
-
return
|
|
44
|
+
async findById<T = any>(slug: string, id: string): Promise<T | null> {
|
|
45
|
+
const store = await this.getStore();
|
|
46
|
+
return store.findById(slug, id);
|
|
54
47
|
}
|
|
55
48
|
|
|
56
|
-
create<T = any>(slug: string, data: Partial<T>): T {
|
|
57
|
-
const
|
|
58
|
-
const now = this.getTimestamp();
|
|
49
|
+
async create<T = any>(slug: string, data: Partial<T>): Promise<T> {
|
|
50
|
+
const store = await this.getStore();
|
|
51
|
+
const now = await this.getTimestamp();
|
|
52
|
+
const id = data?.id || this.generateId();
|
|
59
53
|
const newDoc = {
|
|
60
|
-
id: this.generateId(slug),
|
|
61
54
|
...data,
|
|
55
|
+
id,
|
|
62
56
|
createdAt: now,
|
|
63
57
|
updatedAt: now,
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
this.data.set(slug, docs);
|
|
67
|
-
this.metadata.set(`${slug}:${(newDoc as any).id}`, {
|
|
68
|
-
createdAt: now,
|
|
69
|
-
updatedAt: now,
|
|
70
|
-
});
|
|
71
|
-
return newDoc;
|
|
58
|
+
};
|
|
59
|
+
return store.create(slug, newDoc);
|
|
72
60
|
}
|
|
73
61
|
|
|
74
|
-
update<T = any>(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
async update<T = any>(
|
|
63
|
+
slug: string,
|
|
64
|
+
id: string,
|
|
65
|
+
data: Partial<T>,
|
|
66
|
+
): Promise<T | null> {
|
|
67
|
+
const store = await this.getStore();
|
|
68
|
+
const existing = await store.findById(slug, id);
|
|
69
|
+
if (!existing) return null;
|
|
78
70
|
|
|
79
|
-
const now = this.getTimestamp();
|
|
71
|
+
const now = await this.getTimestamp();
|
|
80
72
|
const updated = {
|
|
81
|
-
...
|
|
73
|
+
...existing,
|
|
82
74
|
...data,
|
|
83
75
|
id,
|
|
84
76
|
updatedAt: now,
|
|
85
77
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
return updated as T;
|
|
78
|
+
|
|
79
|
+
// Save version history before updating
|
|
80
|
+
await this.createVersion(slug, id, existing);
|
|
81
|
+
|
|
82
|
+
return store.update(slug, id, updated);
|
|
93
83
|
}
|
|
94
84
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
if
|
|
85
|
+
async findVersions(parentCollection: string, parentId: string) {
|
|
86
|
+
const store = await this.getStore();
|
|
87
|
+
// We use find with a limit and custom filtering logic if supported,
|
|
88
|
+
// but for now we'll fetch and filter in memory if the adapter is basic
|
|
89
|
+
const result = await store.find(this.VERSIONS_COLLECTION, { limit: 100 });
|
|
90
|
+
return result.docs
|
|
91
|
+
.filter((v: any) => v.parentCollection === parentCollection && v.parentId === parentId)
|
|
92
|
+
.sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async createVersion(parentCollection: string, parentId: string, data: any) {
|
|
96
|
+
const store = await this.getStore();
|
|
97
|
+
const now = await this.getTimestamp();
|
|
98
|
+
const versionDoc = {
|
|
99
|
+
id: this.generateId(),
|
|
100
|
+
parentId,
|
|
101
|
+
parentCollection,
|
|
102
|
+
data,
|
|
103
|
+
createdAt: now,
|
|
104
|
+
version: Date.now() // Simple version numbering
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
return await store.create(this.VERSIONS_COLLECTION, versionDoc);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error("Failed to create version snapshot:", e);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async restoreVersion(parentCollection: string, parentId: string, versionId: string) {
|
|
115
|
+
const store = await this.getStore();
|
|
116
|
+
const version = await store.findById(this.VERSIONS_COLLECTION, versionId);
|
|
117
|
+
if (!version || version.parentId !== parentId) return null;
|
|
118
|
+
|
|
119
|
+
return this.update(parentCollection, parentId, version.data);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async delete(slug: string, id: string): Promise<boolean> {
|
|
123
|
+
const store = await this.getStore();
|
|
124
|
+
return store.delete(slug, id);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async findOne(
|
|
128
|
+
slug: string,
|
|
129
|
+
filter: Record<string, any>,
|
|
130
|
+
): Promise<any | null> {
|
|
131
|
+
const store = await this.getStore();
|
|
132
|
+
const results = await store.find(slug, { limit: 1, where: filter });
|
|
133
|
+
return results.length > 0 ? results[0] : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async findGlobal<T = any>(slug: string): Promise<T> {
|
|
137
|
+
const store = await this.getStore();
|
|
138
|
+
return store.findGlobal(slug);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async updateGlobal<T = any>(slug: string, data: Partial<T>): Promise<T> {
|
|
142
|
+
const store = await this.getStore();
|
|
143
|
+
const current = await store.findGlobal(slug);
|
|
144
|
+
const updated = { ...current, ...data };
|
|
145
|
+
return store.updateGlobal(slug, updated);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async seedGlobal(slug: string, data: any): Promise<void> {
|
|
149
|
+
const store = await this.getStore();
|
|
150
|
+
return store.seedGlobal(slug, data);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async count(slug: string): Promise<number> {
|
|
154
|
+
const store = await this.getStore();
|
|
155
|
+
return store.count(slug);
|
|
156
|
+
}
|
|
99
157
|
|
|
100
|
-
|
|
101
|
-
this.
|
|
102
|
-
|
|
103
|
-
return true;
|
|
158
|
+
async seed(slug: string, docs: any[]): Promise<void> {
|
|
159
|
+
const store = await this.getStore();
|
|
160
|
+
return store.seed(slug, docs);
|
|
104
161
|
}
|
|
105
162
|
|
|
106
|
-
|
|
107
|
-
|
|
163
|
+
async isSeeded(slug: string): Promise<boolean> {
|
|
164
|
+
const store = await this.getStore();
|
|
165
|
+
return store.isSeeded(slug);
|
|
108
166
|
}
|
|
109
167
|
}
|
|
110
168
|
|
|
111
|
-
export const dataStore = new
|
|
169
|
+
export const dataStore = new DataStoreWrapper();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CollectionConfig } from "@kyro-cms/core";
|
|
2
|
+
|
|
3
|
+
export interface DatabaseAdapter {
|
|
4
|
+
initialize(collections: Record<string, CollectionConfig>): void;
|
|
5
|
+
|
|
6
|
+
find<T = any>(
|
|
7
|
+
slug: string,
|
|
8
|
+
options?: { page?: number; limit?: number },
|
|
9
|
+
): Promise<{
|
|
10
|
+
docs: T[];
|
|
11
|
+
totalDocs: number;
|
|
12
|
+
totalPages: number;
|
|
13
|
+
page: number;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
findById<T = any>(slug: string, id: string): Promise<T | null>;
|
|
17
|
+
|
|
18
|
+
create<T = any>(slug: string, data: Partial<T>): Promise<T>;
|
|
19
|
+
|
|
20
|
+
update<T = any>(
|
|
21
|
+
slug: string,
|
|
22
|
+
id: string,
|
|
23
|
+
data: Partial<T>,
|
|
24
|
+
): Promise<T | null>;
|
|
25
|
+
|
|
26
|
+
delete(slug: string, id: string): Promise<boolean>;
|
|
27
|
+
|
|
28
|
+
findGlobal<T = any>(slug: string): Promise<T>;
|
|
29
|
+
|
|
30
|
+
updateGlobal<T = any>(slug: string, data: Partial<T>): Promise<T>;
|
|
31
|
+
|
|
32
|
+
seedGlobal(slug: string, data: any): Promise<void>;
|
|
33
|
+
|
|
34
|
+
count(slug: string): Promise<number>;
|
|
35
|
+
|
|
36
|
+
seed(slug: string, docs: any[]): Promise<void>;
|
|
37
|
+
|
|
38
|
+
isSeeded(slug: string): Promise<boolean>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type DatabaseType = "sqlite" | "postgres" | "mysql" | "mongodb";
|
|
42
|
+
|
|
43
|
+
export interface DatabaseConfig {
|
|
44
|
+
type: DatabaseType;
|
|
45
|
+
connectionString?: string;
|
|
46
|
+
host?: string;
|
|
47
|
+
port?: number;
|
|
48
|
+
database?: string;
|
|
49
|
+
username?: string;
|
|
50
|
+
password?: string;
|
|
51
|
+
poolMin?: number;
|
|
52
|
+
poolMax?: number;
|
|
53
|
+
contentDbPath?: string;
|
|
54
|
+
}
|