@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.
Files changed (163) hide show
  1. package/README.md +149 -51
  2. package/package.json +53 -6
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +136 -27
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +23 -6
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +70 -11
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +200 -139
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +42 -24
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +11 -11
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +13 -13
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
@@ -0,0 +1,305 @@
1
+ import type { AuthAdapter, AuthUser, Session, UserRole } from "@kyro-cms/core";
2
+ import type { AuditLog, AuditLogFilter } from "@kyro-cms/core";
3
+ import { MongoClient, Db, ObjectId } from "mongodb";
4
+ import bcrypt from "bcryptjs";
5
+ import { randomBytes } from "crypto";
6
+
7
+ export interface MongoDBAuthAdapterOptions {
8
+ connectionString?: string;
9
+ host?: string;
10
+ port?: number;
11
+ username?: string;
12
+ password?: string;
13
+ database?: string;
14
+ sessionTTL?: number;
15
+ refreshTokenTTL?: number;
16
+ }
17
+
18
+ const DEFAULT_SESSION_TTL = 86400;
19
+ const DEFAULT_REFRESH_TTL = 604800;
20
+
21
+ export class MongoDBAuthAdapter implements AuthAdapter {
22
+ private client: MongoClient | null = null;
23
+ private db: Db | null = null;
24
+ private sessionTTL: number;
25
+ private refreshTTL: number;
26
+
27
+ constructor(private options: MongoDBAuthAdapterOptions) {
28
+ this.sessionTTL = options.sessionTTL || DEFAULT_SESSION_TTL;
29
+ this.refreshTTL = options.refreshTokenTTL || DEFAULT_REFRESH_TTL;
30
+ }
31
+
32
+ private async getDb(): Promise<Db> {
33
+ if (!this.client) {
34
+ const connectionString =
35
+ this.options.connectionString ||
36
+ `mongodb://${this.options.username}:${this.options.password}@${this.options.host}:${this.options.port}`;
37
+ this.client = new MongoClient(connectionString);
38
+ await this.client.connect();
39
+ }
40
+ if (!this.db) {
41
+ this.db = this.client!.db(this.options.database || "kyro");
42
+ }
43
+ return this.db;
44
+ }
45
+
46
+ private generateId(): string {
47
+ return new ObjectId().toHexString();
48
+ }
49
+
50
+ async createUser(data: {
51
+ email: string;
52
+ passwordHash: string;
53
+ role: UserRole;
54
+ tenantId?: string;
55
+ }): Promise<AuthUser> {
56
+ const db = await this.getDb();
57
+ const now = new Date();
58
+ const id = this.generateId();
59
+
60
+ await db.collection("users").insertOne({
61
+ _id: new ObjectId(id),
62
+ email: data.email.toLowerCase(),
63
+ passwordHash: data.passwordHash,
64
+ name: null,
65
+ role: data.role,
66
+ tenantId: data.tenantId || null,
67
+ emailVerified: false,
68
+ locked: false,
69
+ lastLogin: null,
70
+ failedLoginAttempts: 0,
71
+ lockedUntil: null,
72
+ createdAt: now,
73
+ updatedAt: now,
74
+ });
75
+
76
+ return {
77
+ id,
78
+ email: data.email,
79
+ role: data.role,
80
+ tenantId: data.tenantId,
81
+ createdAt: now.toISOString(),
82
+ updatedAt: now.toISOString(),
83
+ };
84
+ }
85
+
86
+ async findUserByEmail(email: string): Promise<AuthUser | null> {
87
+ const db = await this.getDb();
88
+ const user = await db
89
+ .collection("users")
90
+ .findOne({ email: email.toLowerCase() });
91
+ if (!user) return null;
92
+ return {
93
+ id: user._id.toString(),
94
+ email: user.email,
95
+ role: user.role as UserRole,
96
+ tenantId: user.tenantId || undefined,
97
+ createdAt: user.createdAt?.toISOString() || "",
98
+ updatedAt: user.updatedAt?.toISOString() || "",
99
+ };
100
+ }
101
+
102
+ async findUserById(id: string): Promise<AuthUser | null> {
103
+ const db = await this.getDb();
104
+ const user = await db
105
+ .collection("users")
106
+ .findOne({ _id: new ObjectId(id) });
107
+ if (!user) return null;
108
+ return {
109
+ id: user._id.toString(),
110
+ email: user.email,
111
+ role: user.role as UserRole,
112
+ tenantId: user.tenantId || undefined,
113
+ createdAt: user.createdAt?.toISOString() || "",
114
+ updatedAt: user.updatedAt?.toISOString() || "",
115
+ };
116
+ }
117
+
118
+ async updateUser(
119
+ id: string,
120
+ data: Partial<AuthUser>,
121
+ ): Promise<AuthUser | null> {
122
+ const db = await this.getDb();
123
+ const updates: Record<string, unknown> = { updatedAt: new Date() };
124
+ if (data.email) updates.email = data.email.toLowerCase();
125
+ if (data.role) updates.role = data.role;
126
+ if (data.passwordHash) updates.passwordHash = data.passwordHash;
127
+
128
+ await db
129
+ .collection("users")
130
+ .updateOne({ _id: new ObjectId(id) }, { $set: updates });
131
+
132
+ return this.findUserById(id);
133
+ }
134
+
135
+ async deleteUser(id: string): Promise<boolean> {
136
+ const db = await this.getDb();
137
+ await db.collection("users").deleteOne({ _id: new ObjectId(id) });
138
+ return true;
139
+ }
140
+
141
+ async verifyPassword(password: string, hash: string): Promise<boolean> {
142
+ return bcrypt.compare(password, hash);
143
+ }
144
+
145
+ async hashPassword(password: string): Promise<string> {
146
+ return bcrypt.hash(password, 10);
147
+ }
148
+
149
+ async createSession(
150
+ userId: string,
151
+ data?: { ipAddress?: string; userAgent?: string },
152
+ ): Promise<Session> {
153
+ const db = await this.getDb();
154
+ const now = new Date();
155
+ const expiresAt = new Date(now.getTime() + this.sessionTTL * 1000);
156
+ const token = randomBytes(32).toString("hex");
157
+ const refreshToken = randomBytes(32).toString("hex");
158
+ const id = this.generateId();
159
+
160
+ await db.collection("sessions").insertOne({
161
+ _id: new ObjectId(id),
162
+ token,
163
+ refreshToken,
164
+ userId,
165
+ expiresAt,
166
+ createdAt: now,
167
+ ipAddress: data?.ipAddress,
168
+ userAgent: data?.userAgent,
169
+ });
170
+
171
+ return {
172
+ id,
173
+ token,
174
+ refreshToken: refreshToken || undefined,
175
+ userId,
176
+ expiresAt: expiresAt.toISOString(),
177
+ createdAt: now.toISOString(),
178
+ ipAddress: data?.ipAddress,
179
+ userAgent: data?.userAgent,
180
+ };
181
+ }
182
+
183
+ async findSessionByToken(token: string): Promise<Session | null> {
184
+ const db = await this.getDb();
185
+ const session = await db
186
+ .collection("sessions")
187
+ .findOne({ token, expiresAt: { $gt: new Date() } });
188
+ if (!session) return null;
189
+
190
+ return {
191
+ id: session._id.toString(),
192
+ token: session.token,
193
+ refreshToken: session.refreshToken || undefined,
194
+ userId: session.userId,
195
+ expiresAt: session.expiresAt.toISOString(),
196
+ createdAt: session.createdAt.toISOString(),
197
+ };
198
+ }
199
+
200
+ async deleteSession(sessionId: string): Promise<boolean> {
201
+ const db = await this.getDb();
202
+ await db.collection("sessions").deleteOne({ _id: new ObjectId(sessionId) });
203
+ return true;
204
+ }
205
+
206
+ async deleteUserSessions(userId: string): Promise<number> {
207
+ const db = await this.getDb();
208
+ const result = await db.collection("sessions").deleteMany({ userId });
209
+ return result.deletedCount;
210
+ }
211
+
212
+ async hasAnyUsers(): Promise<boolean> {
213
+ const db = await this.getDb();
214
+ const count = await db.collection("users").countDocuments();
215
+ return count > 0;
216
+ }
217
+
218
+ async findAuditLogs(
219
+ filter: AuditLogFilter,
220
+ ): Promise<{ logs: AuditLog[]; total: number }> {
221
+ const {
222
+ limit = 50,
223
+ offset = 0,
224
+ userId,
225
+ action,
226
+ resource,
227
+ success,
228
+ startDate,
229
+ endDate,
230
+ } = filter;
231
+
232
+ const query: Record<string, unknown> = {};
233
+ if (userId) query.userId = userId;
234
+ if (action) {
235
+ if (Array.isArray(action)) query.action = { $in: action };
236
+ else query.action = action;
237
+ }
238
+ if (resource) query.resource = resource;
239
+ if (success !== undefined) query.success = success;
240
+ if (startDate || endDate) {
241
+ query.createdAt = {};
242
+ if (startDate) (query.createdAt as Record<string, Date>).$gte = startDate;
243
+ if (endDate) (query.createdAt as Record<string, Date>).$lte = endDate;
244
+ }
245
+
246
+ const db = await this.getDb();
247
+ const total = await db.collection("audit_logs").countDocuments(query);
248
+ const logs = await db
249
+ .collection("audit_logs")
250
+ .find(query)
251
+ .sort({ createdAt: -1 })
252
+ .skip(offset)
253
+ .limit(limit)
254
+ .toArray();
255
+
256
+ return {
257
+ logs: logs.map((log: any) => ({
258
+ id: log._id.toString(),
259
+ timestamp: log.createdAt,
260
+ action: log.action as AuditLog["action"],
261
+ userId: log.userId || undefined,
262
+ userEmail: log.userEmail || undefined,
263
+ role: log.role || undefined,
264
+ resource: log.resource,
265
+ resourceId: log.resourceId || undefined,
266
+ ipAddress: log.ipAddress || undefined,
267
+ userAgent: log.userAgent || undefined,
268
+ success: log.success,
269
+ error: log.error || undefined,
270
+ metadata: log.metadata || undefined,
271
+ })),
272
+ total,
273
+ };
274
+ }
275
+
276
+ async createAuditLog(
277
+ data: Omit<AuditLog, "id" | "timestamp">,
278
+ ): Promise<AuditLog> {
279
+ const db = await this.getDb();
280
+ const id = this.generateId();
281
+ const timestamp = new Date();
282
+
283
+ await db.collection("audit_logs").insertOne({
284
+ _id: new ObjectId(id),
285
+ action: data.action,
286
+ userId: data.userId,
287
+ userEmail: data.userEmail,
288
+ role: data.role,
289
+ resource: data.resource,
290
+ resourceId: data.resourceId,
291
+ ipAddress: data.ipAddress,
292
+ userAgent: data.userAgent,
293
+ success: data.success,
294
+ error: data.error,
295
+ metadata: data.metadata,
296
+ createdAt: timestamp,
297
+ });
298
+
299
+ return {
300
+ ...data,
301
+ id,
302
+ timestamp,
303
+ };
304
+ }
305
+ }
@@ -0,0 +1,113 @@
1
+ import {
2
+ mysqlTable,
3
+ varchar,
4
+ boolean,
5
+ timestamp,
6
+ text,
7
+ json,
8
+ index,
9
+ uniqueIndex,
10
+ } from "drizzle-orm/mysql-core";
11
+
12
+ export const users = mysqlTable(
13
+ "users",
14
+ {
15
+ id: varchar("id", { length: 36 }).primaryKey(),
16
+ email: varchar("email", { length: 255 }).notNull(),
17
+ passwordHash: varchar("password_hash", { length: 255 }),
18
+ name: varchar("name", { length: 255 }),
19
+ role: varchar("role", { length: 50 }).notNull().default("customer"),
20
+ tenantId: varchar("tenant_id", { length: 36 }),
21
+ emailVerified: boolean("email_verified").default(false),
22
+ locked: boolean("locked").default(false),
23
+ lastLogin: timestamp("last_login"),
24
+ failedLoginAttempts: varchar("failed_login_attempts", {
25
+ length: 10,
26
+ }).default("0"),
27
+ createdAt: timestamp("created_at").defaultNow().notNull(),
28
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
29
+ },
30
+ (table) => [
31
+ uniqueIndex("users_email_idx").on(table.email),
32
+ index("users_tenant_idx").on(table.tenantId),
33
+ ],
34
+ );
35
+
36
+ export const sessions = mysqlTable(
37
+ "sessions",
38
+ {
39
+ id: varchar("id", { length: 36 }).primaryKey(),
40
+ token: varchar("token", { length: 512 }).notNull().unique(),
41
+ refreshToken: varchar("refresh_token", { length: 512 }),
42
+ userId: varchar("user_id", { length: 36 }).notNull(),
43
+ expiresAt: timestamp("expires_at").notNull(),
44
+ createdAt: timestamp("created_at").defaultNow().notNull(),
45
+ },
46
+ (table) => [
47
+ index("sessions_user_idx").on(table.userId),
48
+ index("sessions_token_idx").on(table.token),
49
+ index("sessions_expires_idx").on(table.expiresAt),
50
+ ],
51
+ );
52
+
53
+ export const roles = mysqlTable(
54
+ "roles",
55
+ {
56
+ id: varchar("id", { length: 36 }).primaryKey(),
57
+ name: varchar("name", { length: 100 }).notNull().unique(),
58
+ level: varchar("level", { length: 10 }).notNull().default("0"),
59
+ inherits: text("inherits"),
60
+ permissions: json("permissions").$type<string[]>(),
61
+ isSystem: boolean("is_system").default(false),
62
+ description: varchar("description", { length: 500 }),
63
+ createdAt: timestamp("created_at").defaultNow().notNull(),
64
+ },
65
+ (table) => [uniqueIndex("roles_name_idx").on(table.name)],
66
+ );
67
+
68
+ export const auditLogs = mysqlTable(
69
+ "audit_logs",
70
+ {
71
+ id: varchar("id", { length: 36 }).primaryKey(),
72
+ action: varchar("action", { length: 100 }).notNull(),
73
+ userId: varchar("user_id", { length: 36 }),
74
+ userEmail: varchar("user_email", { length: 255 }),
75
+ role: varchar("role", { length: 50 }),
76
+ resource: varchar("resource", { length: 255 }).notNull(),
77
+ ipAddress: varchar("ip_address", { length: 45 }),
78
+ userAgent: text("user_agent"),
79
+ success: boolean("success").notNull(),
80
+ error: text("error"),
81
+ metadata: json("metadata").$type<Record<string, unknown>>(),
82
+ createdAt: timestamp("created_at").defaultNow().notNull(),
83
+ },
84
+ (table) => [
85
+ index("audit_user_idx").on(table.userId),
86
+ index("audit_action_idx").on(table.action),
87
+ index("audit_resource_idx").on(table.resource),
88
+ index("audit_created_idx").on(table.createdAt),
89
+ ],
90
+ );
91
+
92
+ export const passwordHistory = mysqlTable(
93
+ "password_history",
94
+ {
95
+ id: varchar("id", { length: 36 }).primaryKey(),
96
+ userId: varchar("user_id", { length: 36 }).notNull(),
97
+ passwordHash: varchar("password_hash", { length: 255 }).notNull(),
98
+ createdAt: timestamp("created_at").defaultNow().notNull(),
99
+ },
100
+ (table) => [index("pwd_history_user_idx").on(table.userId)],
101
+ );
102
+
103
+ export const lockouts = mysqlTable(
104
+ "lockouts",
105
+ {
106
+ id: varchar("id", { length: 36 }).primaryKey(),
107
+ identifier: varchar("identifier", { length: 255 }).notNull(),
108
+ failedAttempts: varchar("failed_attempts", { length: 10 }).default("0"),
109
+ lockedUntil: timestamp("locked_until"),
110
+ createdAt: timestamp("created_at").defaultNow().notNull(),
111
+ },
112
+ (table) => [uniqueIndex("lockouts_identifier_idx").on(table.identifier)],
113
+ );
@@ -0,0 +1,20 @@
1
+ import { mysqlTable, text, timestamp, json } from "drizzle-orm/mysql-core";
2
+
3
+ export const documents = mysqlTable("documents", {
4
+ id: text("id").primaryKey(),
5
+ collection: text("collection").notNull(),
6
+ data: json("data").notNull().$type<Record<string, unknown>>(),
7
+ createdAt: timestamp("created_at").notNull(),
8
+ updatedAt: timestamp("updated_at").notNull(),
9
+ });
10
+
11
+ export const globals = mysqlTable("globals", {
12
+ slug: text("slug").primaryKey(),
13
+ data: json("data").notNull().$type<Record<string, unknown>>(),
14
+ updatedAt: timestamp("updated_at").notNull(),
15
+ });
16
+
17
+ export type Document = typeof documents.$inferSelect;
18
+ export type NewDocument = typeof documents.$inferInsert;
19
+ export type Global = typeof globals.$inferSelect;
20
+ export type NewGlobal = typeof globals.$inferInsert;
@@ -0,0 +1,116 @@
1
+ import {
2
+ pgTable,
3
+ uuid,
4
+ varchar,
5
+ boolean,
6
+ timestamp,
7
+ text,
8
+ jsonb,
9
+ index,
10
+ uniqueIndex,
11
+ } from "drizzle-orm/pg-core";
12
+
13
+ export const users = pgTable(
14
+ "users",
15
+ {
16
+ id: uuid("id").primaryKey().defaultRandom(),
17
+ email: varchar("email", { length: 255 }).notNull(),
18
+ passwordHash: varchar("password_hash", { length: 255 }),
19
+ name: varchar("name", { length: 255 }),
20
+ role: varchar("role", { length: 50 }).notNull().default("customer"),
21
+ tenantId: uuid("tenant_id"),
22
+ emailVerified: boolean("email_verified").default(false),
23
+ locked: boolean("locked").default(false),
24
+ lastLogin: timestamp("last_login"),
25
+ failedLoginAttempts: integer("failed_login_attempts").default(0),
26
+ metadata: jsonb("metadata").$type<Record<string, unknown>>(),
27
+ createdAt: timestamp("created_at").defaultNow().notNull(),
28
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
29
+ },
30
+ (table) => [
31
+ uniqueIndex("users_email_idx").on(table.email),
32
+ index("users_tenant_idx").on(table.tenantId),
33
+ ],
34
+ );
35
+
36
+ export const sessions = pgTable(
37
+ "sessions",
38
+ {
39
+ id: uuid("id").primaryKey().defaultRandom(),
40
+ token: varchar("token", { length: 512 }).notNull().unique(),
41
+ refreshToken: varchar("refresh_token", { length: 512 }),
42
+ userId: uuid("user_id")
43
+ .notNull()
44
+ .references(() => users.id, { onDelete: "cascade" }),
45
+ expiresAt: timestamp("expires_at").notNull(),
46
+ createdAt: timestamp("created_at").defaultNow().notNull(),
47
+ },
48
+ (table) => [
49
+ index("sessions_user_idx").on(table.userId),
50
+ index("sessions_token_idx").on(table.token),
51
+ index("sessions_expires_idx").on(table.expiresAt),
52
+ ],
53
+ );
54
+
55
+ export const roles = pgTable(
56
+ "roles",
57
+ {
58
+ id: uuid("id").primaryKey().defaultRandom(),
59
+ name: varchar("name", { length: 100 }).notNull().unique(),
60
+ level: integer("level").notNull().default(0),
61
+ inherits: text("inherits").array(),
62
+ permissions: text("permissions").array(),
63
+ isSystem: boolean("is_system").default(false),
64
+ description: text("description"),
65
+ createdAt: timestamp("created_at").defaultNow().notNull(),
66
+ },
67
+ (table) => [index("roles_level_idx").on(table.level)],
68
+ );
69
+
70
+ export const auditLogs = pgTable(
71
+ "audit_logs",
72
+ {
73
+ id: uuid("id").primaryKey().defaultRandom(),
74
+ action: varchar("action", { length: 100 }).notNull(),
75
+ userId: uuid("user_id").references(() => users.id, {
76
+ onDelete: "set null",
77
+ }),
78
+ userEmail: varchar("user_email", { length: 255 }),
79
+ role: varchar("role", { length: 50 }),
80
+ resource: varchar("resource", { length: 100 }).notNull(),
81
+ ipAddress: varchar("ip_address", { length: 45 }),
82
+ userAgent: text("user_agent"),
83
+ success: boolean("success").notNull(),
84
+ error: text("error"),
85
+ metadata: jsonb("metadata").$type<Record<string, unknown>>(),
86
+ createdAt: timestamp("created_at").defaultNow().notNull(),
87
+ },
88
+ (table) => [
89
+ index("audit_user_idx").on(table.userId),
90
+ index("audit_action_idx").on(table.action),
91
+ index("audit_created_idx").on(table.createdAt),
92
+ ],
93
+ );
94
+
95
+ export const passwordHistory = pgTable(
96
+ "password_history",
97
+ {
98
+ id: uuid("id").primaryKey().defaultRandom(),
99
+ userId: uuid("user_id")
100
+ .notNull()
101
+ .references(() => users.id, { onDelete: "cascade" }),
102
+ passwordHash: varchar("password_hash", { length: 255 }).notNull(),
103
+ createdAt: timestamp("created_at").defaultNow().notNull(),
104
+ },
105
+ (table) => [index("password_history_user_idx").on(table.userId)],
106
+ );
107
+
108
+ export const lockouts = pgTable("lockouts", {
109
+ id: uuid("id").primaryKey().defaultRandom(),
110
+ userId: uuid("user_id")
111
+ .notNull()
112
+ .references(() => users.id, { onDelete: "cascade" }),
113
+ ipAddress: varchar("ip_address", { length: 45 }),
114
+ lockedUntil: timestamp("locked_until").notNull(),
115
+ createdAt: timestamp("created_at").defaultNow().notNull(),
116
+ });
@@ -0,0 +1,35 @@
1
+ import {
2
+ pgTable,
3
+ uuid,
4
+ text,
5
+ timestamp,
6
+ jsonb,
7
+ index,
8
+ uniqueIndex,
9
+ } from "drizzle-orm/pg-core";
10
+
11
+ export const documents = pgTable(
12
+ "documents",
13
+ {
14
+ id: uuid("id").primaryKey().defaultRandom(),
15
+ collection: text("collection").notNull(),
16
+ data: jsonb("data").notNull().$type<Record<string, unknown>>(),
17
+ createdAt: timestamp("created_at").notNull().defaultNow(),
18
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
19
+ },
20
+ (table) => [
21
+ index("idx_documents_collection").on(table.collection),
22
+ index("idx_documents_created_at").on(table.createdAt),
23
+ ],
24
+ );
25
+
26
+ export const globals = pgTable("globals", {
27
+ slug: text("slug").primaryKey(),
28
+ data: jsonb("data").notNull().$type<Record<string, unknown>>(),
29
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
30
+ });
31
+
32
+ export type Document = typeof documents.$inferSelect;
33
+ export type NewDocument = typeof documents.$inferInsert;
34
+ export type Global = typeof globals.$inferSelect;
35
+ export type NewGlobal = typeof globals.$inferInsert;
@@ -0,0 +1,52 @@
1
+ import {
2
+ pgTable,
3
+ uuid,
4
+ varchar,
5
+ timestamp,
6
+ integer,
7
+ text,
8
+ jsonb,
9
+ index,
10
+ } from "drizzle-orm/pg-core";
11
+
12
+ export const media = pgTable(
13
+ "media",
14
+ {
15
+ id: uuid("id").primaryKey().defaultRandom(),
16
+ filename: varchar("filename", { length: 255 }).notNull().unique(),
17
+ title: varchar("title", { length: 255 }),
18
+ originalName: varchar("original_name", { length: 255 }).notNull(),
19
+ mimeType: varchar("mime_type", { length: 100 }).notNull(),
20
+ fileSize: integer("file_size").notNull(),
21
+ width: integer("width"),
22
+ height: integer("height"),
23
+ url: text("url").notNull().unique(),
24
+ thumbnailUrl: text("thumbnail_url"),
25
+ folder: varchar("folder", { length: 255 }),
26
+ provider: varchar("provider", { length: 50 }).notNull(),
27
+ alt: text("alt"),
28
+ caption: text("caption"),
29
+ metadata: jsonb("metadata").$type<Record<string, unknown>>(),
30
+ createdAt: timestamp("created_at").defaultNow().notNull(),
31
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
32
+ },
33
+ (table) => [
34
+ index("media_folder_idx").on(table.folder),
35
+ index("media_provider_idx").on(table.provider),
36
+ index("media_filename_idx").on(table.filename),
37
+ ],
38
+ );
39
+
40
+ export const mediaFolders = pgTable(
41
+ "media_folders",
42
+ {
43
+ path: varchar("path", { length: 500 }).primaryKey(),
44
+ name: varchar("name", { length: 255 }).notNull(),
45
+ parentPath: varchar("parent_path", { length: 500 }),
46
+ createdAt: timestamp("created_at").defaultNow().notNull(),
47
+ },
48
+ (table) => [index("media_folders_parent_idx").on(table.parentPath)],
49
+ );
50
+
51
+ export type Media = typeof media.$inferSelect;
52
+ export type NewMedia = typeof media.$inferInsert;
@@ -0,0 +1,11 @@
1
+ import { pgTable, varchar, text, timestamp } from "drizzle-orm/pg-core";
2
+
3
+ export const settings = pgTable("settings", {
4
+ key: varchar("key", { length: 255 }).primaryKey(),
5
+ value: text("value").notNull(),
6
+ description: text("description"),
7
+ updatedAt: timestamp("updated_at").defaultNow(),
8
+ });
9
+
10
+ export type Setting = typeof settings.$inferSelect;
11
+ export type NewSetting = typeof settings.$inferInsert;