kilo-cms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -0
- package/drizzle.kilo.config.ts +17 -0
- package/package.json +97 -0
- package/src/admin/AiSettingsEditor.tsx +48 -0
- package/src/admin/AiWritingAssistant.tsx +159 -0
- package/src/admin/ApiTokensEditor.tsx +69 -0
- package/src/admin/ArrayField.tsx +118 -0
- package/src/admin/BlocksField.tsx +131 -0
- package/src/admin/Button.tsx +31 -0
- package/src/admin/CloseButton.tsx +18 -0
- package/src/admin/CmsLocalesProvider.tsx +19 -0
- package/src/admin/CmsSidebar.tsx +147 -0
- package/src/admin/CmsSiteSettingsProvider.tsx +30 -0
- package/src/admin/CmsThemeProvider.tsx +100 -0
- package/src/admin/CollectionListEditor.tsx +278 -0
- package/src/admin/CollectionRecordEditor.tsx +420 -0
- package/src/admin/ContentEditorForm.tsx +21 -0
- package/src/admin/ContentViewSettings.tsx +55 -0
- package/src/admin/DashboardBuilder.tsx +284 -0
- package/src/admin/DragHandle.tsx +3 -0
- package/src/admin/DynamicField.tsx +517 -0
- package/src/admin/EditorHeader.tsx +13 -0
- package/src/admin/EditorSkeleton.tsx +27 -0
- package/src/admin/EmailSettingsEditor.tsx +80 -0
- package/src/admin/Field.tsx +215 -0
- package/src/admin/FilterBuilder.tsx +62 -0
- package/src/admin/GroupField.tsx +61 -0
- package/src/admin/ImageUploader.tsx +198 -0
- package/src/admin/ListingSettingsModal.tsx +68 -0
- package/src/admin/ListingView.tsx +123 -0
- package/src/admin/LocalesEditor.tsx +86 -0
- package/src/admin/LocalizedField.tsx +46 -0
- package/src/admin/MapPicker.tsx +53 -0
- package/src/admin/MediaLibrary.tsx +483 -0
- package/src/admin/MediaUploader.tsx +55 -0
- package/src/admin/Portal.tsx +10 -0
- package/src/admin/PublishingPanel.tsx +102 -0
- package/src/admin/RecordMeta.tsx +24 -0
- package/src/admin/ReviewQueueEditor.tsx +103 -0
- package/src/admin/RichTextEditor.tsx +49 -0
- package/src/admin/RoleEditor.tsx +121 -0
- package/src/admin/RolesEditor.tsx +42 -0
- package/src/admin/RowActionsMenu.tsx +64 -0
- package/src/admin/ScheduledEditor.tsx +155 -0
- package/src/admin/SeoSettingsEditor.tsx +53 -0
- package/src/admin/SingleRecordEditor.tsx +215 -0
- package/src/admin/SiteSettingsEditor.tsx +96 -0
- package/src/admin/Toast.tsx +45 -0
- package/src/admin/UserEditor.tsx +64 -0
- package/src/admin/UsersEditor.tsx +37 -0
- package/src/admin/WebhooksEditor.tsx +115 -0
- package/src/admin/admin.css +6637 -0
- package/src/admin/globals.css +14 -0
- package/src/admin/pages/admin-index.tsx +5 -0
- package/src/admin/pages/ai-settings.tsx +9 -0
- package/src/admin/pages/api-tokens.tsx +10 -0
- package/src/admin/pages/collection-list.tsx +22 -0
- package/src/admin/pages/collection-record.tsx +31 -0
- package/src/admin/pages/dashboard.tsx +10 -0
- package/src/admin/pages/email-settings.tsx +10 -0
- package/src/admin/pages/layout.tsx +24 -0
- package/src/admin/pages/locales-settings.tsx +10 -0
- package/src/admin/pages/login.tsx +25 -0
- package/src/admin/pages/media.tsx +9 -0
- package/src/admin/pages/review-queue.tsx +20 -0
- package/src/admin/pages/role-edit.tsx +11 -0
- package/src/admin/pages/roles.tsx +10 -0
- package/src/admin/pages/scheduled.tsx +17 -0
- package/src/admin/pages/seo-settings.tsx +9 -0
- package/src/admin/pages/setup-admin.tsx +76 -0
- package/src/admin/pages/single-record.tsx +24 -0
- package/src/admin/pages/site-settings.tsx +9 -0
- package/src/admin/pages/user-edit.tsx +11 -0
- package/src/admin/pages/user-new.tsx +10 -0
- package/src/admin/pages/users.tsx +10 -0
- package/src/admin/pages/webhooks-settings.tsx +10 -0
- package/src/admin/useKeyedRows.ts +35 -0
- package/src/ai-config.ts +35 -0
- package/src/ai-secrets.ts +17 -0
- package/src/ai-types.ts +10 -0
- package/src/ai-writer.ts +84 -0
- package/src/api-tokens.ts +25 -0
- package/src/auth-client.ts +5 -0
- package/src/auth.ts +33 -0
- package/src/calendar-grid.ts +12 -0
- package/src/cli/index.mjs +149 -0
- package/src/cms-appearance.ts +55 -0
- package/src/collections/content-view.ts +69 -0
- package/src/collections/define.ts +17 -0
- package/src/collections/filters.ts +92 -0
- package/src/collections/index.ts +11 -0
- package/src/collections/join.ts +64 -0
- package/src/collections/locale.ts +22 -0
- package/src/collections/query.ts +160 -0
- package/src/collections/registry.ts +88 -0
- package/src/collections/schedule.ts +12 -0
- package/src/collections/server.ts +12 -0
- package/src/collections/single.ts +26 -0
- package/src/collections/table.ts +14 -0
- package/src/collections/types.ts +336 -0
- package/src/collections/validation.ts +217 -0
- package/src/collections/values.ts +181 -0
- package/src/collections/workflow.ts +90 -0
- package/src/config.ts +34 -0
- package/src/content-types.ts +218 -0
- package/src/dashboard.ts +39 -0
- package/src/db.ts +50 -0
- package/src/email.ts +39 -0
- package/src/format.ts +4 -0
- package/src/listQuery.ts +23 -0
- package/src/locale-settings.ts +23 -0
- package/src/media-storage.ts +19 -0
- package/src/permissions.ts +37 -0
- package/src/publishing-workflow-db.ts +11 -0
- package/src/publishing-workflow.ts +20 -0
- package/src/richtext.ts +5 -0
- package/src/routes/admin/ai/settings/route.ts +20 -0
- package/src/routes/admin/ai/write/route.ts +21 -0
- package/src/routes/admin/api-tokens/[id]/route.ts +12 -0
- package/src/routes/admin/api-tokens/route.ts +26 -0
- package/src/routes/admin/collections/[collection]/[id]/approve/route.ts +22 -0
- package/src/routes/admin/collections/[collection]/[id]/duplicate/route.ts +36 -0
- package/src/routes/admin/collections/[collection]/[id]/restore/route.ts +19 -0
- package/src/routes/admin/collections/[collection]/[id]/review/route.ts +28 -0
- package/src/routes/admin/collections/[collection]/[id]/route.ts +110 -0
- package/src/routes/admin/collections/[collection]/[id]/schedule/route.ts +42 -0
- package/src/routes/admin/collections/[collection]/[id]/submit-review/route.ts +23 -0
- package/src/routes/admin/collections/[collection]/[id]/unschedule/route.ts +22 -0
- package/src/routes/admin/collections/[collection]/bulk/route.ts +61 -0
- package/src/routes/admin/collections/[collection]/options/route.ts +38 -0
- package/src/routes/admin/collections/[collection]/reorder/route.ts +19 -0
- package/src/routes/admin/collections/[collection]/route.ts +88 -0
- package/src/routes/admin/collections/route.ts +13 -0
- package/src/routes/admin/content-views/[type]/route.ts +25 -0
- package/src/routes/admin/dashboard/data/route.ts +39 -0
- package/src/routes/admin/dashboard/route.ts +30 -0
- package/src/routes/admin/email-settings/route.ts +49 -0
- package/src/routes/admin/email-settings/test/route.ts +18 -0
- package/src/routes/admin/locales/[code]/route.ts +38 -0
- package/src/routes/admin/locales/reorder/route.ts +18 -0
- package/src/routes/admin/locales/route.ts +46 -0
- package/src/routes/admin/media/[id]/restore/route.ts +13 -0
- package/src/routes/admin/media/[id]/route.ts +56 -0
- package/src/routes/admin/media/bulk/route.ts +46 -0
- package/src/routes/admin/media/folders/[id]/route.ts +34 -0
- package/src/routes/admin/media/route.ts +41 -0
- package/src/routes/admin/media/stats/route.ts +14 -0
- package/src/routes/admin/roles/[id]/route.ts +43 -0
- package/src/routes/admin/roles/route.ts +34 -0
- package/src/routes/admin/settings/route.ts +45 -0
- package/src/routes/admin/single/[type]/route.ts +54 -0
- package/src/routes/admin/singles/route.ts +15 -0
- package/src/routes/admin/upload/route.ts +55 -0
- package/src/routes/admin/users/[id]/route.ts +24 -0
- package/src/routes/admin/users/route.ts +24 -0
- package/src/routes/admin/webhooks/[id]/route.ts +38 -0
- package/src/routes/admin/webhooks/route.ts +26 -0
- package/src/routes/cron/publish-scheduled.ts +37 -0
- package/src/routes/setup/admin.ts +19 -0
- package/src/schema/admin-ui.ts +18 -0
- package/src/schema/auth.ts +49 -0
- package/src/schema/developer-tools.ts +44 -0
- package/src/schema/helpers.ts +35 -0
- package/src/schema/index.ts +33 -0
- package/src/schema/locales.ts +13 -0
- package/src/schema/media.ts +22 -0
- package/src/schema/rbac.ts +11 -0
- package/src/schema/settings.ts +28 -0
- package/src/secrets.ts +26 -0
- package/src/seo.ts +69 -0
- package/src/storage/index.ts +20 -0
- package/src/storage/local.ts +27 -0
- package/src/storage/s3-compatible.ts +31 -0
- package/src/storage/types.ts +8 -0
- package/src/storage/vercel-blob.ts +17 -0
- package/src/useBodyScrollLock.ts +12 -0
- package/src/useLocalStorageState.ts +26 -0
- package/src/webhook-events.ts +12 -0
- package/src/webhooks.ts +30 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isNotNull, isNull, sql } from 'drizzle-orm'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { db } from '../../../../db'
|
|
4
|
+
import { media } from '../../../../schema'
|
|
5
|
+
import { getSessionWithPermissions } from '../../../../permissions'
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
if (!await getSessionWithPermissions()) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
9
|
+
const [[active], [trash]] = await Promise.all([
|
|
10
|
+
db.select({ count: sql<number>`count(*)`, bytes: sql<number>`coalesce(sum(${media.size}), 0)` }).from(media).where(isNull(media.deletedAt)),
|
|
11
|
+
db.select({ count: sql<number>`count(*)` }).from(media).where(isNotNull(media.deletedAt)),
|
|
12
|
+
])
|
|
13
|
+
return NextResponse.json({ count: Number(active?.count ?? 0), bytes: Number(active?.bytes ?? 0), trashCount: Number(trash?.count ?? 0) })
|
|
14
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { db } from '../../../../db'
|
|
5
|
+
import { roles } from '../../../../schema'
|
|
6
|
+
import { requireSystem } from '../../../../permissions'
|
|
7
|
+
import { contentTypeKeys, systemPermissions } from '../../../../content-types'
|
|
8
|
+
|
|
9
|
+
const contentTypePermissionSchema = z.object({ actions: z.array(z.enum(['create', 'read', 'update', 'delete', 'publish', 'submit_review', 'review', 'approve'])), fields: z.array(z.string().max(60)) })
|
|
10
|
+
const permissionsSchema = z.object({
|
|
11
|
+
contentTypes: z.record(z.string(), contentTypePermissionSchema).default({}).refine((value) => Object.keys(value).every((key) => (contentTypeKeys as string[]).includes(key)), 'Unknown content type.'),
|
|
12
|
+
system: z.array(z.enum(systemPermissions)).default([]),
|
|
13
|
+
})
|
|
14
|
+
const updateSchema = z.object({ name: z.string().trim().min(1).max(80), permissions: permissionsSchema })
|
|
15
|
+
|
|
16
|
+
export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
17
|
+
if (!await requireSystem('roles.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
18
|
+
const { id } = await params
|
|
19
|
+
const [role] = await db.select().from(roles).where(eq(roles.id, id)).limit(1)
|
|
20
|
+
return role ? NextResponse.json(role) : NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
24
|
+
if (!await requireSystem('roles.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
25
|
+
const { id } = await params
|
|
26
|
+
const [existing] = await db.select({ isSystem: roles.isSystem }).from(roles).where(eq(roles.id, id)).limit(1)
|
|
27
|
+
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
28
|
+
if (existing.isSystem) return NextResponse.json({ error: 'The Administrator role cannot be changed.' }, { status: 403 })
|
|
29
|
+
const parsed = updateSchema.safeParse(await request.json())
|
|
30
|
+
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((issue) => `${issue.path.join(' ')}: ${issue.message}`).join(' · ') }, { status: 400 })
|
|
31
|
+
const [role] = await db.update(roles).set({ name: parsed.data.name, permissions: parsed.data.permissions, updatedAt: new Date() }).where(eq(roles.id, id)).returning()
|
|
32
|
+
return NextResponse.json(role)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function DELETE(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
36
|
+
if (!await requireSystem('roles.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
37
|
+
const { id } = await params
|
|
38
|
+
const [existing] = await db.select({ isSystem: roles.isSystem }).from(roles).where(eq(roles.id, id)).limit(1)
|
|
39
|
+
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
40
|
+
if (existing.isSystem) return NextResponse.json({ error: 'The Administrator role cannot be deleted.' }, { status: 403 })
|
|
41
|
+
await db.delete(roles).where(eq(roles.id, id))
|
|
42
|
+
return NextResponse.json({ ok: true })
|
|
43
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { asc, sql } from 'drizzle-orm'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { db } from '../../../db'
|
|
5
|
+
import { roles, user } from '../../../schema'
|
|
6
|
+
import { requireSystem } from '../../../permissions'
|
|
7
|
+
import { contentTypeKeys, systemPermissions } from '../../../content-types'
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
if (!await requireSystem('roles.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
11
|
+
const [roleRows, userCounts] = await Promise.all([
|
|
12
|
+
db.select().from(roles).orderBy(asc(roles.name)),
|
|
13
|
+
db.select({ role: user.role, count: sql<number>`count(*)::int` }).from(user).groupBy(user.role),
|
|
14
|
+
])
|
|
15
|
+
const counts = new Map(userCounts.map((row: any) => [row.role, row.count]))
|
|
16
|
+
return NextResponse.json(roleRows.map((role: any) => ({ ...role, userCount: counts.get(role.id) ?? 0 })))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const contentTypePermissionSchema = z.object({ actions: z.array(z.enum(['create', 'read', 'update', 'delete', 'publish', 'submit_review', 'review', 'approve'])), fields: z.array(z.string().max(60)) })
|
|
20
|
+
const permissionsSchema = z.object({
|
|
21
|
+
contentTypes: z.record(z.string(), contentTypePermissionSchema).default({}).refine((value) => Object.keys(value).every((key) => (contentTypeKeys as string[]).includes(key)), 'Unknown content type.'),
|
|
22
|
+
system: z.array(z.enum(systemPermissions)).default([]),
|
|
23
|
+
})
|
|
24
|
+
const createSchema = z.object({ id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Use lowercase letters, numbers, and hyphens only').max(60), name: z.string().trim().min(1).max(80), permissions: permissionsSchema.default({ contentTypes: {}, system: [] }) })
|
|
25
|
+
|
|
26
|
+
export async function POST(request: Request) {
|
|
27
|
+
if (!await requireSystem('roles.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
28
|
+
const parsed = createSchema.safeParse(await request.json())
|
|
29
|
+
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((issue) => `${issue.path.join(' ')}: ${issue.message}`).join(' · ') }, { status: 400 })
|
|
30
|
+
try {
|
|
31
|
+
const [role] = await db.insert(roles).values({ id: parsed.data.id, name: parsed.data.name, permissions: parsed.data.permissions, isSystem: false }).returning()
|
|
32
|
+
return NextResponse.json(role, { status: 201 })
|
|
33
|
+
} catch { return NextResponse.json({ error: 'A role with that ID already exists.' }, { status: 409 }) }
|
|
34
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { eq } from 'drizzle-orm'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { db } from '../../../db'
|
|
5
|
+
import { siteSettings } from '../../../schema'
|
|
6
|
+
import { defaultSiteSettings } from '../../../seo'
|
|
7
|
+
import { requireAction } from '../../../permissions'
|
|
8
|
+
import { applyFieldPermissions } from '../../../content-types'
|
|
9
|
+
import { defaultCmsAppearance, normalizeCmsAppearance } from '../../../cms-appearance'
|
|
10
|
+
import { allApprovalContentTypes, defaultPublishingWorkflow, normalizePublishingWorkflow } from '../../../publishing-workflow'
|
|
11
|
+
|
|
12
|
+
const optionalImage = z.union([z.string().url(), z.string().regex(/^\/uploads\/(?:[a-z0-9-]+\/)?[a-z0-9-]+\.(jpg|png|webp)$/), z.literal(''), z.null()]).transform((value) => value || null)
|
|
13
|
+
const tokenMap = z.record(z.string(), z.union([z.string().regex(/^#[0-9a-fA-F]{6}$/), z.number().int().min(0).max(64)])).default({})
|
|
14
|
+
const appearanceSchema = z.object({ preset: z.string().max(40), light: tokenMap, dark: tokenMap }).transform(normalizeCmsAppearance)
|
|
15
|
+
const workflowSchema = z.object({ approvalRequired: z.array(z.enum(allApprovalContentTypes as [string, ...string[]])).default([]) }).transform(normalizePublishingWorkflow)
|
|
16
|
+
const settingsSchema = z.object({
|
|
17
|
+
siteName: z.string().trim().min(1, 'Site name is required').max(80),
|
|
18
|
+
logoLight: optionalImage,
|
|
19
|
+
logoDark: optionalImage,
|
|
20
|
+
metaTitle: z.string().max(160).default(''),
|
|
21
|
+
metaDescription: z.string().max(300).default(''),
|
|
22
|
+
metaImage: optionalImage,
|
|
23
|
+
cmsAppearance: appearanceSchema.default(defaultCmsAppearance),
|
|
24
|
+
publishingWorkflow: workflowSchema.default(defaultPublishingWorkflow),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export async function GET() {
|
|
28
|
+
if (!await requireAction('settings', 'read')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
29
|
+
const [settings] = await db.select().from(siteSettings).where(eq(siteSettings.id, 'default')).limit(1)
|
|
30
|
+
return NextResponse.json(settings || defaultSiteSettings)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function PUT(request: Request) {
|
|
34
|
+
const result = await requireAction('settings', 'update')
|
|
35
|
+
if (!result) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
36
|
+
const body = await request.json()
|
|
37
|
+
const parsed = settingsSchema.safeParse(body)
|
|
38
|
+
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((issue) => `${issue.path.join(' ')}: ${issue.message}`).join(' · ') }, { status: 400 })
|
|
39
|
+
|
|
40
|
+
const [existing] = await db.select().from(siteSettings).where(eq(siteSettings.id, 'default')).limit(1)
|
|
41
|
+
const submitted = applyFieldPermissions('settings', parsed.data, result.allowedFields('settings'), existing ?? defaultSiteSettings)
|
|
42
|
+
const data = { ...submitted, updatedAt: new Date() }
|
|
43
|
+
const [settings] = await db.insert(siteSettings).values({ id: 'default', ...data }).onConflictDoUpdate({ target: siteSettings.id, set: data }).returning()
|
|
44
|
+
return NextResponse.json(settings)
|
|
45
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { requireAction } from '../../../../permissions'
|
|
3
|
+
import { applyFieldPermissions, blankFieldValues, redactFieldsForRead } from '../../../../content-types'
|
|
4
|
+
import { getSingle } from '../../../../collections/registry'
|
|
5
|
+
import { findInvalidRelation, findRowBy, isUniqueViolation } from '../../../../collections/query'
|
|
6
|
+
import { resolveJoinFields } from '../../../../collections/join'
|
|
7
|
+
import { readSingle, writeSingle } from '../../../../collections/single'
|
|
8
|
+
import { parseCollectionBody, type ParseMode } from '../../../../collections/validation'
|
|
9
|
+
import { fireWebhook, updateWorkflowValues } from '../../../../collections/workflow'
|
|
10
|
+
import { getLocaleConfig } from '../../../../locale-settings'
|
|
11
|
+
|
|
12
|
+
const unauthorized = () => NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
13
|
+
const notFound = () => NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
14
|
+
|
|
15
|
+
export async function GET(_request: Request, { params }: { params: Promise<{ type: string }> }) {
|
|
16
|
+
const { type } = await params
|
|
17
|
+
const config = getSingle(type)
|
|
18
|
+
if (!config) return notFound()
|
|
19
|
+
const result = await requireAction(config.slug, 'read')
|
|
20
|
+
if (!result) return unauthorized()
|
|
21
|
+
const row = await resolveJoinFields(config, await readSingle(config))
|
|
22
|
+
return NextResponse.json(redactFieldsForRead(config.slug, row, result.allowedFields(config.slug)))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function PUT(request: Request, { params }: { params: Promise<{ type: string }> }) {
|
|
26
|
+
const { type } = await params
|
|
27
|
+
const config = getSingle(type)
|
|
28
|
+
if (!config) return notFound()
|
|
29
|
+
const result = await requireAction(config.slug, 'update')
|
|
30
|
+
if (!result) return unauthorized()
|
|
31
|
+
|
|
32
|
+
const body = await request.json()
|
|
33
|
+
const wantsPublished = config.workflow?.publish ? Boolean(body.published) : null
|
|
34
|
+
if (wantsPublished && !result.can(config.slug, 'publish')) return unauthorized()
|
|
35
|
+
|
|
36
|
+
const mode: ParseMode = config.workflow?.publish ? (wantsPublished ? 'live' : 'draft') : 'live'
|
|
37
|
+
const parsed = parseCollectionBody(config, body, mode, result.allowedFields(config.slug), await getLocaleConfig())
|
|
38
|
+
if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 })
|
|
39
|
+
|
|
40
|
+
const existing = await findRowBy(config.slug, config.fixedKey.column, config.fixedKey.value)
|
|
41
|
+
const data = applyFieldPermissions(config.slug, parsed.data, result.allowedFields(config.slug), existing ?? blankFieldValues[config.slug])
|
|
42
|
+
|
|
43
|
+
const relationError = await findInvalidRelation(config, data)
|
|
44
|
+
if (relationError) return NextResponse.json({ error: relationError }, { status: 400 })
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const row = await writeSingle(config, data, updateWorkflowValues(config, result.session.user.id, wantsPublished))
|
|
48
|
+
fireWebhook(config, existing ? 'updated' : 'created', row)
|
|
49
|
+
return NextResponse.json(row)
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (isUniqueViolation(error)) return NextResponse.json({ error: 'A record with one of those unique values already exists.' }, { status: 409 })
|
|
52
|
+
throw error
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getSessionWithPermissions } from '../../../permissions'
|
|
3
|
+
import { getSingle, singleSlugs } from '../../../collections/registry'
|
|
4
|
+
import type { SingleConfig } from '../../../collections/types'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const result = await getSessionWithPermissions()
|
|
8
|
+
if (!result) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
9
|
+
const visible = (singleSlugs as string[])
|
|
10
|
+
.map((slug) => getSingle(slug))
|
|
11
|
+
.filter((config): config is SingleConfig => Boolean(config))
|
|
12
|
+
.filter((config) => result.can(config.slug, 'read'))
|
|
13
|
+
.map((config) => ({ slug: config.slug, label: config.label }))
|
|
14
|
+
return NextResponse.json(visible)
|
|
15
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { eq } from 'drizzle-orm'
|
|
3
|
+
import { db } from '../../../db'
|
|
4
|
+
import { media, mediaFolders } from '../../../schema'
|
|
5
|
+
import { getSessionWithPermissions } from '../../../permissions'
|
|
6
|
+
import { uploadFile } from '../../../media-storage'
|
|
7
|
+
|
|
8
|
+
export const runtime = 'nodejs'
|
|
9
|
+
|
|
10
|
+
const extensions: Record<string, string> = {
|
|
11
|
+
'image/jpeg': 'jpg',
|
|
12
|
+
'image/png': 'png',
|
|
13
|
+
'image/webp': 'webp',
|
|
14
|
+
'image/gif': 'gif',
|
|
15
|
+
'image/svg+xml': 'svg',
|
|
16
|
+
'video/mp4': 'mp4',
|
|
17
|
+
'video/webm': 'webm',
|
|
18
|
+
'video/quicktime': 'mov',
|
|
19
|
+
'audio/mpeg': 'mp3',
|
|
20
|
+
'audio/wav': 'wav',
|
|
21
|
+
'audio/ogg': 'ogg',
|
|
22
|
+
'application/pdf': 'pdf',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Per-category ceilings — video/audio need far more headroom than a compressed image.
|
|
26
|
+
const maxBytesByPrefix: Array<[string, number]> = [
|
|
27
|
+
['image/', 8 * 1024 * 1024],
|
|
28
|
+
['video/', 80 * 1024 * 1024],
|
|
29
|
+
['audio/', 25 * 1024 * 1024],
|
|
30
|
+
]
|
|
31
|
+
const defaultMaxBytes = 20 * 1024 * 1024 // pdf and anything else in the allowlist
|
|
32
|
+
|
|
33
|
+
function maxBytesFor(mimeType: string) {
|
|
34
|
+
return maxBytesByPrefix.find(([prefix]) => mimeType.startsWith(prefix))?.[1] ?? defaultMaxBytes
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function POST(request: Request) {
|
|
38
|
+
if (!await getSessionWithPermissions()) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
39
|
+
|
|
40
|
+
const formData = await request.formData()
|
|
41
|
+
const file = formData.get('file')
|
|
42
|
+
const requestedFolder = String(formData.get('folder') || 'uploads')
|
|
43
|
+
if (!file || typeof file === 'string' || !('arrayBuffer' in file) || !('type' in file) || !(file.type in extensions)) return NextResponse.json({ error: 'Unsupported file type.' }, { status: 400 })
|
|
44
|
+
const maxBytes = maxBytesFor(file.type)
|
|
45
|
+
if (file.size > maxBytes) return NextResponse.json({ error: `File must be smaller than ${Math.round(maxBytes / (1024 * 1024))} MB.` }, { status: 400 })
|
|
46
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(requestedFolder)) return NextResponse.json({ error: 'Invalid media folder.' }, { status: 400 })
|
|
47
|
+
if (requestedFolder !== 'uploads') { const [folder] = await db.select().from(mediaFolders).where(eq(mediaFolders.slug, requestedFolder)).limit(1); if (!folder) return NextResponse.json({ error: 'Media folder not found.' }, { status: 400 }) }
|
|
48
|
+
|
|
49
|
+
// Storage is flat — `folder` is pure metadata for organizing the media library, never part
|
|
50
|
+
// of the file's key, so moving a file between folders later can never break its URL.
|
|
51
|
+
const { url } = await uploadFile(Buffer.from(await file.arrayBuffer()), file.type, extensions[file.type])
|
|
52
|
+
const filename = url.split('/').pop() ?? url
|
|
53
|
+
await db.insert(media).values({ id: crypto.randomUUID(), filename, url, folder: requestedFolder, mimeType: file.type, size: file.size })
|
|
54
|
+
return NextResponse.json({ url })
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { db } from '../../../../db'
|
|
5
|
+
import { user } from '../../../../schema'
|
|
6
|
+
import { requireSystem } from '../../../../permissions'
|
|
7
|
+
|
|
8
|
+
export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
9
|
+
if (!await requireSystem('users.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
10
|
+
const { id } = await params
|
|
11
|
+
const [row] = await db.select({ id: user.id, name: user.name, email: user.email, role: user.role }).from(user).where(eq(user.id, id)).limit(1)
|
|
12
|
+
return row ? NextResponse.json(row) : NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const schema = z.object({ name: z.string().trim().min(1).max(120).optional(), email: z.string().trim().email().optional(), role: z.string().min(1).max(60).optional() })
|
|
16
|
+
|
|
17
|
+
export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
18
|
+
if (!await requireSystem('users.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
19
|
+
const { id } = await params
|
|
20
|
+
const parsed = schema.safeParse(await request.json())
|
|
21
|
+
if (!parsed.success) return NextResponse.json({ error: 'Invalid request.' }, { status: 400 })
|
|
22
|
+
const [row] = await db.update(user).set(parsed.data).where(eq(user.id, id)).returning({ id: user.id, name: user.name, email: user.email, role: user.role })
|
|
23
|
+
return row ? NextResponse.json(row) : NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { asc, eq } from 'drizzle-orm'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { auth, db } from '../../../db'
|
|
5
|
+
import { user } from '../../../schema'
|
|
6
|
+
import { getSessionWithPermissions, requireSystem } from '../../../permissions'
|
|
7
|
+
|
|
8
|
+
export async function GET() {
|
|
9
|
+
if (!await getSessionWithPermissions()) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
10
|
+
const rows = await db.select({ id: user.id, name: user.name, email: user.email, role: user.role }).from(user).orderBy(asc(user.name))
|
|
11
|
+
return NextResponse.json(rows)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const createSchema = z.object({ name: z.string().trim().min(1).max(120), email: z.string().trim().email(), password: z.string().min(12).max(200), role: z.string().min(1).max(60) })
|
|
15
|
+
|
|
16
|
+
export async function POST(request: Request) {
|
|
17
|
+
if (!await requireSystem('users.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
18
|
+
const parsed = createSchema.safeParse(await request.json())
|
|
19
|
+
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((issue) => `${issue.path.join(' ')}: ${issue.message}`).join(' · ') }, { status: 400 })
|
|
20
|
+
const result = await auth.api.signUpEmail({ body: { name: parsed.data.name, email: parsed.data.email, password: parsed.data.password } })
|
|
21
|
+
if (!result.user) return NextResponse.json({ error: 'Could not create user — email may already be in use.' }, { status: 400 })
|
|
22
|
+
await db.update(user).set({ role: parsed.data.role }).where(eq(user.id, result.user.id))
|
|
23
|
+
return NextResponse.json({ id: result.user.id, name: parsed.data.name, email: parsed.data.email, role: parsed.data.role }, { status: 201 })
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { desc, eq } from 'drizzle-orm'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { db } from '../../../../db'
|
|
5
|
+
import { webhookDeliveries, webhooks } from '../../../../schema'
|
|
6
|
+
import { requireSystem } from '../../../../permissions'
|
|
7
|
+
import { decryptSecret } from '../../../../secrets'
|
|
8
|
+
import { allWebhookEvents, WEBHOOK_SECRET_DOMAIN } from '../../../../webhooks'
|
|
9
|
+
|
|
10
|
+
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
11
|
+
if (!await requireSystem('developers.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
12
|
+
const { id } = await params
|
|
13
|
+
const [webhook] = await db.select().from(webhooks).where(eq(webhooks.id, id)).limit(1)
|
|
14
|
+
if (!webhook) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
15
|
+
const deliveries = await db.select().from(webhookDeliveries).where(eq(webhookDeliveries.webhookId, id)).orderBy(desc(webhookDeliveries.createdAt)).limit(20)
|
|
16
|
+
let secret = ''
|
|
17
|
+
try { secret = decryptSecret(webhook.encryptedSecret, WEBHOOK_SECRET_DOMAIN) } catch { /* stored before secrets existed, or corrupted — leave blank */ }
|
|
18
|
+
return NextResponse.json({ id: webhook.id, name: webhook.name, url: webhook.url, events: webhook.events, enabled: webhook.enabled, secret, deliveries })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const schema = z.object({ name: z.string().trim().min(1).max(80), url: z.string().url(), events: z.array(z.enum(allWebhookEvents as [string, ...string[]])).max(allWebhookEvents.length), enabled: z.boolean() })
|
|
22
|
+
|
|
23
|
+
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
24
|
+
if (!await requireSystem('developers.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
25
|
+
const { id } = await params
|
|
26
|
+
const parsed = schema.safeParse(await request.json())
|
|
27
|
+
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((issue) => `${issue.path.join(' ')}: ${issue.message}`).join(' · ') }, { status: 400 })
|
|
28
|
+
const [webhook] = await db.update(webhooks).set({ name: parsed.data.name, url: parsed.data.url, events: parsed.data.events, enabled: parsed.data.enabled, updatedAt: new Date() }).where(eq(webhooks.id, id)).returning()
|
|
29
|
+
if (!webhook) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
30
|
+
return NextResponse.json({ id: webhook.id, name: webhook.name, url: webhook.url, events: webhook.events, enabled: webhook.enabled })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
34
|
+
if (!await requireSystem('developers.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
35
|
+
const { id } = await params
|
|
36
|
+
await db.delete(webhooks).where(eq(webhooks.id, id))
|
|
37
|
+
return NextResponse.json({ ok: true })
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { desc } from 'drizzle-orm'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { randomBytes } from 'node:crypto'
|
|
5
|
+
import { db } from '../../../db'
|
|
6
|
+
import { webhooks } from '../../../schema'
|
|
7
|
+
import { requireSystem } from '../../../permissions'
|
|
8
|
+
import { encryptSecret } from '../../../secrets'
|
|
9
|
+
import { allWebhookEvents, WEBHOOK_SECRET_DOMAIN } from '../../../webhooks'
|
|
10
|
+
|
|
11
|
+
export async function GET() {
|
|
12
|
+
if (!await requireSystem('developers.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
13
|
+
const rows = await db.select({ id: webhooks.id, name: webhooks.name, url: webhooks.url, events: webhooks.events, enabled: webhooks.enabled, createdAt: webhooks.createdAt }).from(webhooks).orderBy(desc(webhooks.createdAt))
|
|
14
|
+
return NextResponse.json(rows)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const schema = z.object({ name: z.string().trim().min(1).max(80), url: z.string().url(), events: z.array(z.enum(allWebhookEvents as [string, ...string[]])).max(allWebhookEvents.length), enabled: z.boolean().default(true) })
|
|
18
|
+
|
|
19
|
+
export async function POST(request: Request) {
|
|
20
|
+
if (!await requireSystem('developers.manage')) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
21
|
+
const parsed = schema.safeParse(await request.json())
|
|
22
|
+
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((issue) => `${issue.path.join(' ')}: ${issue.message}`).join(' · ') }, { status: 400 })
|
|
23
|
+
const secret = randomBytes(24).toString('base64url')
|
|
24
|
+
const [webhook] = await db.insert(webhooks).values({ id: crypto.randomUUID(), name: parsed.data.name, url: parsed.data.url, events: parsed.data.events, enabled: parsed.data.enabled, encryptedSecret: encryptSecret(secret, WEBHOOK_SECRET_DOMAIN) }).returning()
|
|
25
|
+
return NextResponse.json({ id: webhook.id, name: webhook.name, url: webhook.url, events: webhook.events, enabled: webhook.enabled, secret }, { status: 201 })
|
|
26
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { and, eq, isNotNull, isNull, lte } from 'drizzle-orm'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { entries, workflowSlugs } from '../../collections/registry'
|
|
4
|
+
import { collectionColumn, updateWhere } from '../../collections/query'
|
|
5
|
+
|
|
6
|
+
export const runtime = 'nodejs'
|
|
7
|
+
|
|
8
|
+
export async function GET(request: Request) {
|
|
9
|
+
const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
|
10
|
+
if (!token || token !== process.env.CRON_SECRET) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
11
|
+
|
|
12
|
+
const now = new Date()
|
|
13
|
+
const byType: Array<{ slug: string; published: number; ids: string[] }> = []
|
|
14
|
+
|
|
15
|
+
for (const slug of workflowSlugs) {
|
|
16
|
+
const config = entries[slug]
|
|
17
|
+
if (!config.workflow?.scheduling) continue
|
|
18
|
+
const scheduledAt = collectionColumn(slug, 'scheduledAt')
|
|
19
|
+
const published = collectionColumn(slug, 'published')
|
|
20
|
+
if (!scheduledAt || !published) continue
|
|
21
|
+
const deletedAt = config.workflow.trash ? collectionColumn(slug, 'deletedAt') : null
|
|
22
|
+
|
|
23
|
+
const rows = await updateWhere(
|
|
24
|
+
slug,
|
|
25
|
+
and(eq(published, false), ...(deletedAt ? [isNull(deletedAt)] : []), isNotNull(scheduledAt), lte(scheduledAt, now))!,
|
|
26
|
+
{ published: true, publishedAt: now, scheduledAt: null },
|
|
27
|
+
)
|
|
28
|
+
byType.push({ slug, published: rows.length, ids: rows.map((row) => String(row.id)) })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return NextResponse.json({
|
|
32
|
+
ok: true,
|
|
33
|
+
published: byType.reduce((sum, item) => sum + item.published, 0),
|
|
34
|
+
ids: byType.flatMap((item) => item.ids),
|
|
35
|
+
byType,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { eq } from 'drizzle-orm'
|
|
3
|
+
import { auth, db } from '../../db'
|
|
4
|
+
import { user } from '../../schema'
|
|
5
|
+
|
|
6
|
+
export async function POST(request: Request) {
|
|
7
|
+
const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
|
8
|
+
if (!token || token !== process.env.CMS_SETUP_TOKEN) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
9
|
+
|
|
10
|
+
const body = await request.json() as { name?: string; email?: string; password?: string }
|
|
11
|
+
if (!body.name || !body.email || !body.password || body.password.length < 12) {
|
|
12
|
+
return NextResponse.json({ error: 'name, email, and a password of at least 12 characters are required' }, { status: 400 })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const result = await auth.api.signUpEmail({ body: { name: body.name, email: body.email, password: body.password } })
|
|
16
|
+
if (!result.user) return NextResponse.json({ error: 'Could not create user' }, { status: 400 })
|
|
17
|
+
await db.update(user).set({ role: 'admin' }).where(eq(user.id, result.user.id))
|
|
18
|
+
return NextResponse.json({ ok: true, userId: result.user.id })
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsonb, pgTable, text } from 'drizzle-orm/pg-core'
|
|
2
|
+
import { timestamps } from './helpers'
|
|
3
|
+
import type { ContentViewSettings } from '../collections/content-view'
|
|
4
|
+
import type { DashboardConfig } from '../dashboard'
|
|
5
|
+
|
|
6
|
+
export const contentViews = pgTable('cms_content_views', {
|
|
7
|
+
contentType: text('content_type').primaryKey(),
|
|
8
|
+
config: jsonb('config').$type<ContentViewSettings>().notNull(),
|
|
9
|
+
...timestamps,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const dashboards = pgTable('cms_dashboards', {
|
|
13
|
+
id: text('id').primaryKey(),
|
|
14
|
+
name: text('name').notNull().default('Main dashboard'),
|
|
15
|
+
config: jsonb('config').$type<DashboardConfig>().notNull(),
|
|
16
|
+
createdBy: text('created_by'),
|
|
17
|
+
...timestamps,
|
|
18
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { boolean, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
|
|
2
|
+
import { timestamps } from './helpers'
|
|
3
|
+
|
|
4
|
+
// Better Auth tables (user/session/account/verification) — column shapes are dictated by the
|
|
5
|
+
// auth library, not by this project's conventions. Keep them together; they only ever change
|
|
6
|
+
// together when the auth adapter changes.
|
|
7
|
+
|
|
8
|
+
export const user = pgTable('user', {
|
|
9
|
+
id: text('id').primaryKey(),
|
|
10
|
+
name: text('name').notNull(),
|
|
11
|
+
email: text('email').notNull().unique(),
|
|
12
|
+
emailVerified: boolean('email_verified').default(false).notNull(),
|
|
13
|
+
image: text('image'),
|
|
14
|
+
role: text('role').default('editor').notNull(),
|
|
15
|
+
...timestamps,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const session = pgTable('session', {
|
|
19
|
+
id: text('id').primaryKey(),
|
|
20
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
21
|
+
token: text('token').notNull().unique(),
|
|
22
|
+
ipAddress: text('ip_address'),
|
|
23
|
+
userAgent: text('user_agent'),
|
|
24
|
+
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
|
25
|
+
...timestamps,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const account = pgTable('account', {
|
|
29
|
+
id: text('id').primaryKey(),
|
|
30
|
+
accountId: text('account_id').notNull(),
|
|
31
|
+
providerId: text('provider_id').notNull(),
|
|
32
|
+
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
|
33
|
+
accessToken: text('access_token'),
|
|
34
|
+
refreshToken: text('refresh_token'),
|
|
35
|
+
idToken: text('id_token'),
|
|
36
|
+
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
|
37
|
+
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
|
38
|
+
scope: text('scope'),
|
|
39
|
+
password: text('password'),
|
|
40
|
+
...timestamps,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const verification = pgTable('verification', {
|
|
44
|
+
id: text('id').primaryKey(),
|
|
45
|
+
identifier: text('identifier').notNull(),
|
|
46
|
+
value: text('value').notNull(),
|
|
47
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
48
|
+
...timestamps,
|
|
49
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { boolean, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
|
|
2
|
+
import { timestamps } from './helpers'
|
|
3
|
+
|
|
4
|
+
export const emailSettings = pgTable('cms_email_settings', {
|
|
5
|
+
id: text('id').primaryKey(),
|
|
6
|
+
host: text('host'),
|
|
7
|
+
port: integer('port'),
|
|
8
|
+
secure: boolean('secure').notNull().default(true),
|
|
9
|
+
username: text('username'),
|
|
10
|
+
encryptedPassword: text('encrypted_password'),
|
|
11
|
+
fromAddress: text('from_address'),
|
|
12
|
+
fromName: text('from_name'),
|
|
13
|
+
...timestamps,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const apiTokens = pgTable('cms_api_tokens', {
|
|
17
|
+
id: text('id').primaryKey(),
|
|
18
|
+
name: text('name').notNull(),
|
|
19
|
+
tokenHash: text('token_hash').notNull().unique(),
|
|
20
|
+
tokenPrefix: text('token_prefix').notNull(),
|
|
21
|
+
lastUsedAt: timestamp('last_used_at'),
|
|
22
|
+
createdBy: text('created_by'),
|
|
23
|
+
...timestamps,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const webhooks = pgTable('cms_webhooks', {
|
|
27
|
+
id: text('id').primaryKey(),
|
|
28
|
+
name: text('name').notNull(),
|
|
29
|
+
url: text('url').notNull(),
|
|
30
|
+
encryptedSecret: text('encrypted_secret').notNull(),
|
|
31
|
+
events: jsonb('events').$type<string[]>().notNull().default([]),
|
|
32
|
+
enabled: boolean('enabled').notNull().default(true),
|
|
33
|
+
...timestamps,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const webhookDeliveries = pgTable('cms_webhook_deliveries', {
|
|
37
|
+
id: text('id').primaryKey(),
|
|
38
|
+
webhookId: text('webhook_id').notNull().references(() => webhooks.id, { onDelete: 'cascade' }),
|
|
39
|
+
event: text('event').notNull(),
|
|
40
|
+
statusCode: integer('status_code'),
|
|
41
|
+
success: boolean('success').notNull(),
|
|
42
|
+
error: text('error'),
|
|
43
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
44
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { boolean, integer, text, timestamp } from 'drizzle-orm/pg-core'
|
|
2
|
+
|
|
3
|
+
export const timestamps = {
|
|
4
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
5
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const workflowColumns = {
|
|
9
|
+
createdBy: text('created_by'),
|
|
10
|
+
ownerId: text('owner_id'),
|
|
11
|
+
updatedBy: text('updated_by'),
|
|
12
|
+
reviewerId: text('reviewer_id'),
|
|
13
|
+
approverId: text('approver_id'),
|
|
14
|
+
reviewStatus: text('review_status').notNull().default('draft'),
|
|
15
|
+
reviewedAt: timestamp('reviewed_at'),
|
|
16
|
+
approvedAt: timestamp('approved_at'),
|
|
17
|
+
published: boolean('published').notNull().default(false),
|
|
18
|
+
publishedAt: timestamp('published_at'),
|
|
19
|
+
deletedAt: timestamp('deleted_at'),
|
|
20
|
+
sortOrder: integer('sort_order').notNull().default(0),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const publishColumns = {
|
|
24
|
+
createdBy: text('created_by'),
|
|
25
|
+
ownerId: text('owner_id'),
|
|
26
|
+
updatedBy: text('updated_by'),
|
|
27
|
+
published: boolean('published').notNull().default(false),
|
|
28
|
+
publishedAt: timestamp('published_at'),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Engine-owned scheduling column. Spread into any table whose entry declares
|
|
32
|
+
* `workflow.scheduling`. Never declare `scheduledAt` as a FieldConfig. */
|
|
33
|
+
export const schedulingColumns = {
|
|
34
|
+
scheduledAt: timestamp('scheduled_at', { withTimezone: true }),
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Barrel of kilo-cms's OWN tables only — auth, RBAC, media, settings, dev-tools, admin-ui,
|
|
2
|
+
// locales. A host app's own content tables (services/projects/whatever a project defines via
|
|
3
|
+
// defineCollectionTable) live in the HOST's own schema, never here. The host's own schema
|
|
4
|
+
// barrel re-exports this one (`export * from 'kilo-cms/schema'`) so drizzle-kit sees both sets
|
|
5
|
+
// of tables in one merged diff target — see apps/site/drizzle.app.config.ts's comment for why
|
|
6
|
+
// that matters.
|
|
7
|
+
export * from './helpers'
|
|
8
|
+
export * from './auth'
|
|
9
|
+
export * from './rbac'
|
|
10
|
+
export * from './media'
|
|
11
|
+
export * from './settings'
|
|
12
|
+
export * from './developer-tools'
|
|
13
|
+
export * from './admin-ui'
|
|
14
|
+
export * from './locales'
|
|
15
|
+
|
|
16
|
+
import { user, session, account, verification } from './auth'
|
|
17
|
+
import { roles } from './rbac'
|
|
18
|
+
import { mediaFolders, media } from './media'
|
|
19
|
+
import { siteSettings, aiSettings } from './settings'
|
|
20
|
+
import { emailSettings, apiTokens, webhooks, webhookDeliveries } from './developer-tools'
|
|
21
|
+
import { contentViews, dashboards } from './admin-ui'
|
|
22
|
+
import { cmsLocales } from './locales'
|
|
23
|
+
|
|
24
|
+
// The object better-auth's drizzle adapter needs, and the base every host schema barrel
|
|
25
|
+
// spreads into its own merged `schema` object.
|
|
26
|
+
export const schema = {
|
|
27
|
+
user, session, account, verification, roles,
|
|
28
|
+
mediaFolders, media,
|
|
29
|
+
siteSettings, aiSettings,
|
|
30
|
+
emailSettings, apiTokens, webhooks, webhookDeliveries,
|
|
31
|
+
contentViews, dashboards,
|
|
32
|
+
cmsLocales,
|
|
33
|
+
}
|