@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,194 @@
|
|
|
1
|
+
import type { CollectionConfig } from "@kyro-cms/core";
|
|
2
|
+
import type { DatabaseAdapter, DatabaseConfig } from "./adapter";
|
|
3
|
+
import mysql from "mysql2/promise";
|
|
4
|
+
import { drizzle } from "drizzle-orm/mysql2";
|
|
5
|
+
import { eq, desc, sql, and } from "drizzle-orm";
|
|
6
|
+
import * as contentSchema from "./schema/mysql-content";
|
|
7
|
+
|
|
8
|
+
export class MySQLAdapter implements DatabaseAdapter {
|
|
9
|
+
private pool: mysql.Pool;
|
|
10
|
+
private db = drizzle(this.pool);
|
|
11
|
+
private initialized = false;
|
|
12
|
+
|
|
13
|
+
constructor(private config: DatabaseConfig) {
|
|
14
|
+
this.pool = mysql.createPool({
|
|
15
|
+
connectionString: config.connectionString,
|
|
16
|
+
host: config.host,
|
|
17
|
+
port: config.port,
|
|
18
|
+
database: config.database,
|
|
19
|
+
user: config.username,
|
|
20
|
+
password: config.password,
|
|
21
|
+
waitForConnections: true,
|
|
22
|
+
connectionLimit: config.poolMax || 10,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
initialize(collections: Record<string, CollectionConfig>) {
|
|
27
|
+
if (this.initialized) return;
|
|
28
|
+
this.initialized = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private getTimestamp(): Date {
|
|
32
|
+
return new Date();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async generateId(slug: string): Promise<string> {
|
|
36
|
+
const result = await this.db
|
|
37
|
+
.select({ count: sql<number>`count(*)` })
|
|
38
|
+
.from(contentSchema.documents)
|
|
39
|
+
.where(eq(contentSchema.documents.collection, slug));
|
|
40
|
+
return `${slug}-${(result[0]?.count || 0) + 1}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async find<T = any>(
|
|
44
|
+
slug: string,
|
|
45
|
+
options: { page?: number; limit?: number } = {},
|
|
46
|
+
): Promise<{
|
|
47
|
+
docs: T[];
|
|
48
|
+
totalDocs: number;
|
|
49
|
+
totalPages: number;
|
|
50
|
+
page: number;
|
|
51
|
+
}> {
|
|
52
|
+
const page = options.page || 1;
|
|
53
|
+
const limit = options.limit || 25;
|
|
54
|
+
const start = (page - 1) * limit;
|
|
55
|
+
|
|
56
|
+
const countResult = await this.db
|
|
57
|
+
.select({ count: sql<number>`count(*)` })
|
|
58
|
+
.from(contentSchema.documents)
|
|
59
|
+
.where(eq(contentSchema.documents.collection, slug));
|
|
60
|
+
const count = countResult[0]?.count || 0;
|
|
61
|
+
|
|
62
|
+
const docs = await this.db
|
|
63
|
+
.select({ data: contentSchema.documents.data })
|
|
64
|
+
.from(contentSchema.documents)
|
|
65
|
+
.where(eq(contentSchema.documents.collection, slug))
|
|
66
|
+
.orderBy(desc(contentSchema.documents.createdAt))
|
|
67
|
+
.limit(limit)
|
|
68
|
+
.offset(start);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
docs: docs.map((d) => d.data as T),
|
|
72
|
+
totalDocs: count,
|
|
73
|
+
totalPages: Math.ceil(count / limit),
|
|
74
|
+
page,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async findById<T = any>(slug: string, id: string): Promise<T | null> {
|
|
79
|
+
const doc = await this.db
|
|
80
|
+
.select({ data: contentSchema.documents.data })
|
|
81
|
+
.from(contentSchema.documents)
|
|
82
|
+
.where(
|
|
83
|
+
and(
|
|
84
|
+
eq(contentSchema.documents.collection, slug),
|
|
85
|
+
eq(contentSchema.documents.id, id),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
.get();
|
|
89
|
+
return (doc?.data as T) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async create<T = any>(slug: string, data: Partial<T>): Promise<T> {
|
|
93
|
+
const now = this.getTimestamp();
|
|
94
|
+
const id = await this.generateId(slug);
|
|
95
|
+
const newDoc = {
|
|
96
|
+
id,
|
|
97
|
+
collection: slug,
|
|
98
|
+
data: { id, ...data, createdAt: now, updatedAt: now },
|
|
99
|
+
createdAt: now,
|
|
100
|
+
updatedAt: now,
|
|
101
|
+
};
|
|
102
|
+
await this.db.insert(contentSchema.documents).values(newDoc as any);
|
|
103
|
+
return newDoc.data as T;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async update<T = any>(
|
|
107
|
+
slug: string,
|
|
108
|
+
id: string,
|
|
109
|
+
data: Partial<T>,
|
|
110
|
+
): Promise<T | null> {
|
|
111
|
+
const existing = await this.findById<T>(slug, id);
|
|
112
|
+
if (!existing) return null;
|
|
113
|
+
const now = this.getTimestamp();
|
|
114
|
+
const updated = { ...existing, ...data, id, updatedAt: now };
|
|
115
|
+
await this.db
|
|
116
|
+
.update(contentSchema.documents)
|
|
117
|
+
.set({ data: updated, updatedAt: now })
|
|
118
|
+
.where(
|
|
119
|
+
and(
|
|
120
|
+
eq(contentSchema.documents.collection, slug),
|
|
121
|
+
eq(contentSchema.documents.id, id),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
return updated as T;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async delete(slug: string, id: string): Promise<boolean> {
|
|
128
|
+
await this.db
|
|
129
|
+
.delete(contentSchema.documents)
|
|
130
|
+
.where(
|
|
131
|
+
and(
|
|
132
|
+
eq(contentSchema.documents.collection, slug),
|
|
133
|
+
eq(contentSchema.documents.id, id),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async findGlobal<T = any>(slug: string): Promise<T> {
|
|
140
|
+
const global = await this.db
|
|
141
|
+
.select({ data: contentSchema.globals.data })
|
|
142
|
+
.from(contentSchema.globals)
|
|
143
|
+
.where(eq(contentSchema.globals.slug, slug))
|
|
144
|
+
.get();
|
|
145
|
+
return (global?.data as T) || ({} as T);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async updateGlobal<T = any>(slug: string, data: Partial<T>): Promise<T> {
|
|
149
|
+
const now = this.getTimestamp();
|
|
150
|
+
const current = await this.findGlobal<T>(slug);
|
|
151
|
+
const updated = { ...current, ...data };
|
|
152
|
+
await this.db
|
|
153
|
+
.insert(contentSchema.globals)
|
|
154
|
+
.values({ slug, data: updated, updatedAt: now })
|
|
155
|
+
.onDuplicateKeyUpdate({ set: { data: updated, updatedAt: now } });
|
|
156
|
+
return updated as T;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async seedGlobal(slug: string, data: any): Promise<void> {
|
|
160
|
+
const now = this.getTimestamp();
|
|
161
|
+
await this.db
|
|
162
|
+
.insert(contentSchema.globals)
|
|
163
|
+
.values({ slug, data, updatedAt: now })
|
|
164
|
+
.onDuplicateKeyUpdate({ set: { data: data } });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async count(slug: string): Promise<number> {
|
|
168
|
+
const result = await this.db
|
|
169
|
+
.select({ count: sql<number>`count(*)` })
|
|
170
|
+
.from(contentSchema.documents)
|
|
171
|
+
.where(eq(contentSchema.documents.collection, slug));
|
|
172
|
+
return result[0]?.count || 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async seed(slug: string, docs: any[]): Promise<void> {
|
|
176
|
+
const now = this.getTimestamp();
|
|
177
|
+
const values = docs.map((doc, i) => ({
|
|
178
|
+
id: doc.id || `${slug}-${i + 1}`,
|
|
179
|
+
collection: slug,
|
|
180
|
+
data: { ...doc, createdAt: now, updatedAt: now },
|
|
181
|
+
createdAt: now,
|
|
182
|
+
updatedAt: now,
|
|
183
|
+
}));
|
|
184
|
+
await this.db.insert(contentSchema.documents).values(values as any);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async isSeeded(slug: string): Promise<boolean> {
|
|
188
|
+
return (await this.count(slug)) > 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async close(): Promise<void> {
|
|
192
|
+
await this.pool.end();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { AuthAdapter, AuthUser, Session, UserRole } from "@kyro-cms/core";
|
|
2
|
+
import type { AuditLog, AuditLogFilter } from "@kyro-cms/core";
|
|
3
|
+
import mysql from "mysql2/promise";
|
|
4
|
+
import { drizzle } from "drizzle-orm/mysql2";
|
|
5
|
+
import { eq, and, desc, sql } from "drizzle-orm";
|
|
6
|
+
import bcrypt from "bcryptjs";
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import * as authSchema from "./schema/mysql-auth";
|
|
9
|
+
|
|
10
|
+
export interface MySQLAuthAdapterOptions {
|
|
11
|
+
connectionString?: string;
|
|
12
|
+
host?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
username?: string;
|
|
15
|
+
password?: string;
|
|
16
|
+
database?: string;
|
|
17
|
+
sessionTTL?: number;
|
|
18
|
+
refreshTokenTTL?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_SESSION_TTL = 86400;
|
|
22
|
+
const DEFAULT_REFRESH_TTL = 604800;
|
|
23
|
+
|
|
24
|
+
export class MySQLAuthAdapter implements AuthAdapter {
|
|
25
|
+
private pool: mysql.Pool;
|
|
26
|
+
private db;
|
|
27
|
+
private sessionTTL: number;
|
|
28
|
+
private refreshTTL: number;
|
|
29
|
+
|
|
30
|
+
constructor(private options: MySQLAuthAdapterOptions) {
|
|
31
|
+
this.pool = mysql.createPool({
|
|
32
|
+
uri: options.connectionString,
|
|
33
|
+
host: options.host,
|
|
34
|
+
port: options.port,
|
|
35
|
+
database: options.database,
|
|
36
|
+
user: options.username,
|
|
37
|
+
password: options.password,
|
|
38
|
+
waitForConnections: true,
|
|
39
|
+
connectionLimit: 10,
|
|
40
|
+
});
|
|
41
|
+
this.db = drizzle(this.pool);
|
|
42
|
+
this.sessionTTL = options.sessionTTL || DEFAULT_SESSION_TTL;
|
|
43
|
+
this.refreshTTL = options.refreshTokenTTL || DEFAULT_REFRESH_TTL;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private generateId(): string {
|
|
47
|
+
return crypto.randomUUID();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async createUser(data: {
|
|
51
|
+
email: string;
|
|
52
|
+
passwordHash: string;
|
|
53
|
+
role: UserRole;
|
|
54
|
+
tenantId?: string;
|
|
55
|
+
}): Promise<AuthUser> {
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const id = this.generateId();
|
|
58
|
+
await this.db.insert(authSchema.users).values({
|
|
59
|
+
id,
|
|
60
|
+
email: data.email.toLowerCase(),
|
|
61
|
+
passwordHash: data.passwordHash,
|
|
62
|
+
name: null,
|
|
63
|
+
role: data.role,
|
|
64
|
+
tenantId: data.tenantId,
|
|
65
|
+
emailVerified: false,
|
|
66
|
+
locked: false,
|
|
67
|
+
lastLogin: null,
|
|
68
|
+
createdAt: now,
|
|
69
|
+
updatedAt: now,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const [user] = await this.db
|
|
73
|
+
.select()
|
|
74
|
+
.from(authSchema.users)
|
|
75
|
+
.where(eq(authSchema.users.id, id))
|
|
76
|
+
.limit(1);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
id: user.id,
|
|
80
|
+
email: user.email,
|
|
81
|
+
role: user.role as UserRole,
|
|
82
|
+
tenantId: user.tenantId || undefined,
|
|
83
|
+
createdAt: user.createdAt.toISOString(),
|
|
84
|
+
updatedAt: user.updatedAt.toISOString(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async findUserByEmail(email: string): Promise<AuthUser | null> {
|
|
89
|
+
const [user] = await this.db
|
|
90
|
+
.select()
|
|
91
|
+
.from(authSchema.users)
|
|
92
|
+
.where(eq(authSchema.users.email, email.toLowerCase()))
|
|
93
|
+
.limit(1);
|
|
94
|
+
if (!user) return null;
|
|
95
|
+
return {
|
|
96
|
+
id: user.id,
|
|
97
|
+
email: user.email,
|
|
98
|
+
role: user.role as UserRole,
|
|
99
|
+
tenantId: user.tenantId || undefined,
|
|
100
|
+
createdAt: user.createdAt.toISOString(),
|
|
101
|
+
updatedAt: user.updatedAt.toISOString(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async findUserById(id: string): Promise<AuthUser | null> {
|
|
106
|
+
const [user] = await this.db
|
|
107
|
+
.select()
|
|
108
|
+
.from(authSchema.users)
|
|
109
|
+
.where(eq(authSchema.users.id, id))
|
|
110
|
+
.limit(1);
|
|
111
|
+
if (!user) return null;
|
|
112
|
+
return {
|
|
113
|
+
id: user.id,
|
|
114
|
+
email: user.email,
|
|
115
|
+
role: user.role as UserRole,
|
|
116
|
+
tenantId: user.tenantId || undefined,
|
|
117
|
+
createdAt: user.createdAt.toISOString(),
|
|
118
|
+
updatedAt: user.updatedAt.toISOString(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async updateUser(
|
|
123
|
+
id: string,
|
|
124
|
+
data: Partial<AuthUser>,
|
|
125
|
+
): Promise<AuthUser | null> {
|
|
126
|
+
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
|
127
|
+
if (data.email) updates.email = data.email.toLowerCase();
|
|
128
|
+
if (data.role) updates.role = data.role;
|
|
129
|
+
if (data.passwordHash) updates.passwordHash = data.passwordHash;
|
|
130
|
+
|
|
131
|
+
await this.db
|
|
132
|
+
.update(authSchema.users)
|
|
133
|
+
.set(updates)
|
|
134
|
+
.where(eq(authSchema.users.id, id));
|
|
135
|
+
|
|
136
|
+
const [user] = await this.db
|
|
137
|
+
.select()
|
|
138
|
+
.from(authSchema.users)
|
|
139
|
+
.where(eq(authSchema.users.id, id))
|
|
140
|
+
.limit(1);
|
|
141
|
+
if (!user) return null;
|
|
142
|
+
return {
|
|
143
|
+
id: user.id,
|
|
144
|
+
email: user.email,
|
|
145
|
+
role: user.role as UserRole,
|
|
146
|
+
tenantId: user.tenantId || undefined,
|
|
147
|
+
createdAt: user.createdAt.toISOString(),
|
|
148
|
+
updatedAt: user.updatedAt.toISOString(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async deleteUser(id: string): Promise<boolean> {
|
|
153
|
+
await this.db.delete(authSchema.users).where(eq(authSchema.users.id, id));
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
158
|
+
return bcrypt.compare(password, hash);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async hashPassword(password: string): Promise<string> {
|
|
162
|
+
return bcrypt.hash(password, 10);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async createSession(
|
|
166
|
+
userId: string,
|
|
167
|
+
data?: { ipAddress?: string; userAgent?: string },
|
|
168
|
+
): Promise<Session> {
|
|
169
|
+
const now = new Date();
|
|
170
|
+
const expiresAt = new Date(now.getTime() + this.sessionTTL * 1000);
|
|
171
|
+
const token = randomBytes(32).toString("hex");
|
|
172
|
+
const refreshToken = randomBytes(32).toString("hex");
|
|
173
|
+
const id = this.generateId();
|
|
174
|
+
|
|
175
|
+
await this.db.insert(authSchema.sessions).values({
|
|
176
|
+
id,
|
|
177
|
+
token,
|
|
178
|
+
refreshToken,
|
|
179
|
+
userId,
|
|
180
|
+
expiresAt,
|
|
181
|
+
createdAt: now,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
id,
|
|
186
|
+
token,
|
|
187
|
+
refreshToken: refreshToken || undefined,
|
|
188
|
+
userId,
|
|
189
|
+
expiresAt: expiresAt.toISOString(),
|
|
190
|
+
createdAt: now.toISOString(),
|
|
191
|
+
ipAddress: data?.ipAddress,
|
|
192
|
+
userAgent: data?.userAgent,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async findSessionByToken(token: string): Promise<Session | null> {
|
|
197
|
+
const [session] = await this.db
|
|
198
|
+
.select()
|
|
199
|
+
.from(authSchema.sessions)
|
|
200
|
+
.where(eq(authSchema.sessions.token, token))
|
|
201
|
+
.limit(1);
|
|
202
|
+
|
|
203
|
+
if (!session || new Date(session.expiresAt) < new Date()) return null;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
id: session.id,
|
|
207
|
+
token: session.token,
|
|
208
|
+
refreshToken: session.refreshToken || undefined,
|
|
209
|
+
userId: session.userId,
|
|
210
|
+
expiresAt: session.expiresAt.toISOString(),
|
|
211
|
+
createdAt: session.createdAt.toISOString(),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async deleteSession(sessionId: string): Promise<boolean> {
|
|
216
|
+
await this.db
|
|
217
|
+
.delete(authSchema.sessions)
|
|
218
|
+
.where(eq(authSchema.sessions.token, sessionId));
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async deleteUserSessions(userId: string): Promise<number> {
|
|
223
|
+
await this.db
|
|
224
|
+
.delete(authSchema.sessions)
|
|
225
|
+
.where(eq(authSchema.sessions.userId, userId));
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async hasAnyUsers(): Promise<boolean> {
|
|
230
|
+
const [result] = await this.db
|
|
231
|
+
.select({ count: sql<number>`count(*)` })
|
|
232
|
+
.from(authSchema.users);
|
|
233
|
+
return Number(result?.count || 0) > 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async findAuditLogs(
|
|
237
|
+
filter: AuditLogFilter,
|
|
238
|
+
): Promise<{ logs: AuditLog[]; total: number }> {
|
|
239
|
+
const {
|
|
240
|
+
limit = 50,
|
|
241
|
+
offset = 0,
|
|
242
|
+
userId,
|
|
243
|
+
action,
|
|
244
|
+
success,
|
|
245
|
+
startDate,
|
|
246
|
+
endDate,
|
|
247
|
+
} = filter;
|
|
248
|
+
|
|
249
|
+
const conditions = [];
|
|
250
|
+
if (userId) conditions.push(eq(authSchema.auditLogs.userId, userId));
|
|
251
|
+
if (action) {
|
|
252
|
+
if (Array.isArray(action)) {
|
|
253
|
+
conditions.push(
|
|
254
|
+
sql`${authSchema.auditLogs.action} IN (${action.join(",")})`,
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
conditions.push(eq(authSchema.auditLogs.action, action));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (success !== undefined)
|
|
261
|
+
conditions.push(eq(authSchema.auditLogs.success, success));
|
|
262
|
+
if (startDate)
|
|
263
|
+
conditions.push(sql`${authSchema.auditLogs.createdAt} >= ${startDate}`);
|
|
264
|
+
if (endDate)
|
|
265
|
+
conditions.push(sql`${authSchema.auditLogs.createdAt} <= ${endDate}`);
|
|
266
|
+
|
|
267
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
268
|
+
|
|
269
|
+
const countResult = await this.db
|
|
270
|
+
.select({ count: sql<number>`count(*)` })
|
|
271
|
+
.from(authSchema.auditLogs)
|
|
272
|
+
.where(whereClause);
|
|
273
|
+
|
|
274
|
+
const logs = await this.db
|
|
275
|
+
.select()
|
|
276
|
+
.from(authSchema.auditLogs)
|
|
277
|
+
.where(whereClause)
|
|
278
|
+
.orderBy(desc(authSchema.auditLogs.createdAt))
|
|
279
|
+
.limit(limit)
|
|
280
|
+
.offset(offset);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
logs: logs.map((log) => ({
|
|
284
|
+
id: log.id,
|
|
285
|
+
timestamp: log.createdAt,
|
|
286
|
+
action: log.action as AuditLog["action"],
|
|
287
|
+
userId: log.userId || undefined,
|
|
288
|
+
userEmail: log.userEmail || undefined,
|
|
289
|
+
role: log.role || undefined,
|
|
290
|
+
resource: log.resource,
|
|
291
|
+
ipAddress: log.ipAddress || undefined,
|
|
292
|
+
userAgent: log.userAgent || undefined,
|
|
293
|
+
success: log.success,
|
|
294
|
+
error: log.error || undefined,
|
|
295
|
+
metadata: log.metadata || undefined,
|
|
296
|
+
})),
|
|
297
|
+
total: Number(countResult[0]?.count || 0),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async createAuditLog(
|
|
302
|
+
data: Omit<AuditLog, "id" | "timestamp">,
|
|
303
|
+
): Promise<AuditLog> {
|
|
304
|
+
const id = this.generateId();
|
|
305
|
+
const timestamp = new Date();
|
|
306
|
+
|
|
307
|
+
await this.db.insert(authSchema.auditLogs).values({
|
|
308
|
+
id,
|
|
309
|
+
action: data.action,
|
|
310
|
+
userId: data.userId,
|
|
311
|
+
userEmail: data.userEmail,
|
|
312
|
+
role: data.role,
|
|
313
|
+
resource: data.resource,
|
|
314
|
+
ipAddress: data.ipAddress,
|
|
315
|
+
userAgent: data.userAgent,
|
|
316
|
+
success: data.success,
|
|
317
|
+
error: data.error,
|
|
318
|
+
metadata: data.metadata,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
...data,
|
|
323
|
+
id,
|
|
324
|
+
timestamp,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { CollectionConfig } from "@kyro-cms/core";
|
|
2
|
+
import type { DatabaseAdapter, DatabaseConfig } from "./adapter";
|
|
3
|
+
import { Pool } from "pg";
|
|
4
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
5
|
+
import { eq, desc, sql, and } from "drizzle-orm";
|
|
6
|
+
import { randomBytes } from "crypto";
|
|
7
|
+
import * as contentSchema from "./schema/postgres-content";
|
|
8
|
+
|
|
9
|
+
export class PostgresAdapter implements DatabaseAdapter {
|
|
10
|
+
private pool: Pool;
|
|
11
|
+
private db = drizzle(this.pool);
|
|
12
|
+
private initialized = false;
|
|
13
|
+
|
|
14
|
+
constructor(private config: DatabaseConfig) {
|
|
15
|
+
this.pool = new Pool({
|
|
16
|
+
connectionString: config.connectionString,
|
|
17
|
+
host: config.host,
|
|
18
|
+
port: config.port,
|
|
19
|
+
database: config.database,
|
|
20
|
+
user: config.username,
|
|
21
|
+
password: config.password,
|
|
22
|
+
min: config.poolMin || 5,
|
|
23
|
+
max: config.poolMax || 20,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
initialize(collections: Record<string, CollectionConfig>) {
|
|
28
|
+
if (this.initialized) return;
|
|
29
|
+
this.initialized = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private getTimestamp(): string {
|
|
33
|
+
return new Date().toISOString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async generateId(slug: string): Promise<string> {
|
|
37
|
+
const result = await this.db
|
|
38
|
+
.select({ count: sql<number>`count(*)` })
|
|
39
|
+
.from(contentSchema.documents)
|
|
40
|
+
.where(eq(contentSchema.documents.collection, slug));
|
|
41
|
+
return `${slug}-${(result[0]?.count || 0) + 1}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async find<T = any>(
|
|
45
|
+
slug: string,
|
|
46
|
+
options: { page?: number; limit?: number } = {},
|
|
47
|
+
): Promise<{
|
|
48
|
+
docs: T[];
|
|
49
|
+
totalDocs: number;
|
|
50
|
+
totalPages: number;
|
|
51
|
+
page: number;
|
|
52
|
+
}> {
|
|
53
|
+
const page = options.page || 1;
|
|
54
|
+
const limit = options.limit || 25;
|
|
55
|
+
const start = (page - 1) * limit;
|
|
56
|
+
|
|
57
|
+
const countResult = await this.db
|
|
58
|
+
.select({ count: sql<number>`count(*)` })
|
|
59
|
+
.from(contentSchema.documents)
|
|
60
|
+
.where(eq(contentSchema.documents.collection, slug));
|
|
61
|
+
const count = countResult[0]?.count || 0;
|
|
62
|
+
|
|
63
|
+
const docs = await this.db
|
|
64
|
+
.select({ data: contentSchema.documents.data })
|
|
65
|
+
.from(contentSchema.documents)
|
|
66
|
+
.where(eq(contentSchema.documents.collection, slug))
|
|
67
|
+
.orderBy(desc(contentSchema.documents.createdAt))
|
|
68
|
+
.limit(limit)
|
|
69
|
+
.offset(start);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
docs: docs.map((d) => d.data as T),
|
|
73
|
+
totalDocs: count,
|
|
74
|
+
totalPages: Math.ceil(count / limit),
|
|
75
|
+
page,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async findById<T = any>(slug: string, id: string): Promise<T | null> {
|
|
80
|
+
const doc = await this.db
|
|
81
|
+
.select({ data: contentSchema.documents.data })
|
|
82
|
+
.from(contentSchema.documents)
|
|
83
|
+
.where(
|
|
84
|
+
and(
|
|
85
|
+
eq(contentSchema.documents.collection, slug),
|
|
86
|
+
eq(contentSchema.documents.id, id),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
.get();
|
|
90
|
+
return (doc?.data as T) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async create<T = any>(slug: string, data: Partial<T>): Promise<T> {
|
|
94
|
+
const now = this.getTimestamp();
|
|
95
|
+
const id = await this.generateId(slug);
|
|
96
|
+
const newDoc = {
|
|
97
|
+
id,
|
|
98
|
+
collection: slug,
|
|
99
|
+
data: { id, ...data, createdAt: now, updatedAt: now },
|
|
100
|
+
createdAt: new Date(now),
|
|
101
|
+
updatedAt: new Date(now),
|
|
102
|
+
};
|
|
103
|
+
await this.db.insert(contentSchema.documents).values(newDoc as any);
|
|
104
|
+
return newDoc.data as T;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async update<T = any>(
|
|
108
|
+
slug: string,
|
|
109
|
+
id: string,
|
|
110
|
+
data: Partial<T>,
|
|
111
|
+
): Promise<T | null> {
|
|
112
|
+
const existing = await this.findById<T>(slug, id);
|
|
113
|
+
if (!existing) return null;
|
|
114
|
+
const now = this.getTimestamp();
|
|
115
|
+
const updated = { ...existing, ...data, id, updatedAt: now };
|
|
116
|
+
await this.db
|
|
117
|
+
.update(contentSchema.documents)
|
|
118
|
+
.set({ data: updated, updatedAt: new Date(now) })
|
|
119
|
+
.where(
|
|
120
|
+
and(
|
|
121
|
+
eq(contentSchema.documents.collection, slug),
|
|
122
|
+
eq(contentSchema.documents.id, id),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
return updated as T;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async delete(slug: string, id: string): Promise<boolean> {
|
|
129
|
+
await this.db
|
|
130
|
+
.delete(contentSchema.documents)
|
|
131
|
+
.where(
|
|
132
|
+
and(
|
|
133
|
+
eq(contentSchema.documents.collection, slug),
|
|
134
|
+
eq(contentSchema.documents.id, id),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async findGlobal<T = any>(slug: string): Promise<T> {
|
|
141
|
+
const global = await this.db
|
|
142
|
+
.select({ data: contentSchema.globals.data })
|
|
143
|
+
.from(contentSchema.globals)
|
|
144
|
+
.where(eq(contentSchema.globals.slug, slug))
|
|
145
|
+
.get();
|
|
146
|
+
return (global?.data as T) || ({} as T);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async updateGlobal<T = any>(slug: string, data: Partial<T>): Promise<T> {
|
|
150
|
+
const now = this.getTimestamp();
|
|
151
|
+
const current = await this.findGlobal<T>(slug);
|
|
152
|
+
const updated = { ...current, ...data };
|
|
153
|
+
await this.db
|
|
154
|
+
.insert(contentSchema.globals)
|
|
155
|
+
.values({ slug, data: updated, updatedAt: new Date(now) })
|
|
156
|
+
.onConflictDoUpdate({
|
|
157
|
+
target: contentSchema.globals.slug,
|
|
158
|
+
set: { data: updated, updatedAt: new Date(now) },
|
|
159
|
+
});
|
|
160
|
+
return updated as T;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async seedGlobal(slug: string, data: any): Promise<void> {
|
|
164
|
+
const now = this.getTimestamp();
|
|
165
|
+
await this.db
|
|
166
|
+
.insert(contentSchema.globals)
|
|
167
|
+
.values({ slug, data, updatedAt: new Date(now) })
|
|
168
|
+
.onConflictDoNothing();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async count(slug: string): Promise<number> {
|
|
172
|
+
const result = await this.db
|
|
173
|
+
.select({ count: sql<number>`count(*)` })
|
|
174
|
+
.from(contentSchema.documents)
|
|
175
|
+
.where(eq(contentSchema.documents.collection, slug));
|
|
176
|
+
return result[0]?.count || 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async seed(slug: string, docs: any[]): Promise<void> {
|
|
180
|
+
const now = this.getTimestamp();
|
|
181
|
+
const values = docs.map((doc, i) => ({
|
|
182
|
+
id: doc.id || `${slug}-${i + 1}`,
|
|
183
|
+
collection: slug,
|
|
184
|
+
data: {
|
|
185
|
+
...doc,
|
|
186
|
+
createdAt: doc.createdAt || now,
|
|
187
|
+
updatedAt: doc.updatedAt || now,
|
|
188
|
+
},
|
|
189
|
+
createdAt: new Date(now),
|
|
190
|
+
updatedAt: new Date(now),
|
|
191
|
+
}));
|
|
192
|
+
await this.db.insert(contentSchema.documents).values(values as any);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async isSeeded(slug: string): Promise<boolean> {
|
|
196
|
+
return (await this.count(slug)) > 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async close(): Promise<void> {
|
|
200
|
+
await this.pool.end();
|
|
201
|
+
}
|
|
202
|
+
}
|