mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 19:44:12 +00:00
chore: code cleanup, TypeScript fixes, and dependency updates
- Remove unused variables and imports across API routes and workers - Fix TypeScript errors in ConsentLogs.ts (PayloadRequest header access) - Fix TypeScript errors in formSubmissionHooks.ts (add ResponseTracking interface) - Update eslint ignores for coverage, test results, and generated files - Set push: false in payload.config.ts (schema changes only via migrations) - Update dependencies to latest versions (Payload 3.68.4, React 19.2.3) - Add framework update check script and documentation - Regenerate payload-types.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b3440cae5
commit
2faefdac1e
22 changed files with 1561 additions and 1199 deletions
|
|
@ -21,6 +21,7 @@
|
||||||
| [x] | Staging-Deployment | DevOps |
|
| [x] | Staging-Deployment | DevOps |
|
||||||
| [x] | Memory-Problem lösen (Swap) | Infrastruktur |
|
| [x] | Memory-Problem lösen (Swap) | Infrastruktur |
|
||||||
| [ ] | PM2 Cluster Mode testen | Infrastruktur |
|
| [ ] | PM2 Cluster Mode testen | Infrastruktur |
|
||||||
|
| [ ] | Payload/Next Releases auf Next.js 16 Support beobachten *(siehe `framework-monitoring.md`)* | Tech Debt |
|
||||||
|
|
||||||
### Niedrige Priorität
|
### Niedrige Priorität
|
||||||
| Status | Task | Bereich |
|
| Status | Task | Bereich |
|
||||||
|
|
|
||||||
33
docs/anleitungen/framework-monitoring.md
Normal file
33
docs/anleitungen/framework-monitoring.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Framework Monitoring – Next.js & Payload
|
||||||
|
|
||||||
|
Dieser Leitfaden beschreibt, wie wir beobachten, wann Payload offiziell Next.js 16 (oder spätere) Versionen unterstützt und wann wir die Upgrades wieder aufnehmen können.
|
||||||
|
|
||||||
|
## 1. Wöchentlicher Versions-Check
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm check:frameworks
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Befehl führt `pnpm outdated` nur für Payload-Core und alle Payload-Plugins sowie Next.js aus. Damit sehen wir sofort, ob es neue Veröffentlichungen gibt, die wir evaluieren sollten.
|
||||||
|
|
||||||
|
> Falls du den Check auf CI ausführen möchtest, stelle sicher, dass `pnpm` installiert ist und das Repository bereits `pnpm install` ausgeführt hat.
|
||||||
|
|
||||||
|
## 2. Release Notes verfolgen
|
||||||
|
|
||||||
|
- Payload Releases: https://github.com/payloadcms/payload/releases
|
||||||
|
Abonniere die Repo-Releases („Watch → Releases only“), damit du automatisch benachrichtigt wirst, wenn ein neues Release Next.js 16 als kompatibel markiert.
|
||||||
|
- Next.js Blog: https://nextjs.org/blog
|
||||||
|
Relevant, um Breaking Changes zu erkennen, die Payload evtl. erst später unterstützt.
|
||||||
|
|
||||||
|
## 3. Vorgehen bei neuem Payload-Release
|
||||||
|
|
||||||
|
1. `pnpm check:frameworks` ausführen und prüfen, ob `@payloadcms/next` oder `@payloadcms/ui` eine neue Version anbieten, deren Peer-Dependencies `next@16` erlauben.
|
||||||
|
2. Falls ja:
|
||||||
|
- Branch erstellen (`feature/upgrade-next16`)
|
||||||
|
- `package.json` anpassen (Next.js + Payload) und `pnpm install`
|
||||||
|
- `pnpm lint`, `pnpm typecheck`, `pnpm test` und ein Test-Build (`pnpm build && pnpm test:e2e` falls vorhanden) ausführen.
|
||||||
|
3. Läuft alles fehlerfrei, kann das Update über PR/Merge in `develop`.
|
||||||
|
|
||||||
|
## 4. Erinnerung
|
||||||
|
|
||||||
|
In der To-Do-Liste (`docs/anleitungen/TODO.md`) gibt es einen Eintrag „Payload/Next Releases auf Next.js 16 Support beobachten“. Wenn das Upgrade abgeschlossen ist, kann dieser Task auf erledigt gesetzt werden.
|
||||||
|
|
@ -41,6 +41,11 @@ const eslintConfig = [
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
'.next/',
|
'.next/',
|
||||||
|
'coverage/',
|
||||||
|
'node_modules/',
|
||||||
|
'playwright-report/',
|
||||||
|
'test-results/',
|
||||||
|
'next-env.d.ts',
|
||||||
'src/migrations/', // Payload migrations have required but unused params
|
'src/migrations/', // Payload migrations have required but unused params
|
||||||
'src/migrations_backup/',
|
'src/migrations_backup/',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
61
package.json
61
package.json
|
|
@ -10,7 +10,8 @@
|
||||||
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||||
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
"lint": "cross-env NODE_OPTIONS=--no-deprecation eslint src",
|
||||||
|
"check:frameworks": "bash ./scripts/check-framework-updates.sh",
|
||||||
"typecheck": "cross-env NODE_OPTIONS=--no-deprecation tsc --noEmit",
|
"typecheck": "cross-env NODE_OPTIONS=--no-deprecation tsc --noEmit",
|
||||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx}\" --ignore-unknown",
|
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx}\" --ignore-unknown",
|
||||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx}\" --ignore-unknown",
|
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx}\" --ignore-unknown",
|
||||||
|
|
@ -26,50 +27,50 @@
|
||||||
"prepare": "test -d .git && (ln -sf ../../scripts/detect-secrets.sh .git/hooks/pre-commit 2>/dev/null || true) || true"
|
"prepare": "test -d .git && (ln -sf ../../scripts/detect-secrets.sh .git/hooks/pre-commit 2>/dev/null || true) || true"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@payloadcms/db-postgres": "3.65.0",
|
"@payloadcms/db-postgres": "3.68.4",
|
||||||
"@payloadcms/next": "3.65.0",
|
"@payloadcms/next": "3.68.4",
|
||||||
"@payloadcms/plugin-form-builder": "3.65.0",
|
"@payloadcms/plugin-form-builder": "3.68.4",
|
||||||
"@payloadcms/plugin-multi-tenant": "^3.65.0",
|
"@payloadcms/plugin-multi-tenant": "3.68.4",
|
||||||
"@payloadcms/plugin-nested-docs": "3.65.0",
|
"@payloadcms/plugin-nested-docs": "3.68.4",
|
||||||
"@payloadcms/plugin-redirects": "3.65.0",
|
"@payloadcms/plugin-redirects": "3.68.4",
|
||||||
"@payloadcms/plugin-seo": "3.65.0",
|
"@payloadcms/plugin-seo": "3.68.4",
|
||||||
"@payloadcms/richtext-lexical": "3.65.0",
|
"@payloadcms/richtext-lexical": "3.68.4",
|
||||||
"@payloadcms/translations": "^3.65.0",
|
"@payloadcms/translations": "3.68.4",
|
||||||
"@payloadcms/ui": "3.65.0",
|
"@payloadcms/ui": "3.68.4",
|
||||||
"bullmq": "^5.65.1",
|
"bullmq": "^5.65.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "16.4.7",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"ioredis": "^5.8.2",
|
"ioredis": "^5.8.2",
|
||||||
"next": "15.4.8",
|
"next": "15.5.9",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"nodemailer": "^7.0.11",
|
"nodemailer": "^7.0.11",
|
||||||
"payload": "3.65.0",
|
"payload": "3.68.4",
|
||||||
"payload-oapi": "^0.2.5",
|
"payload-oapi": "^0.2.5",
|
||||||
"react": "19.2.1",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.1",
|
"react-dom": "19.2.3",
|
||||||
"sharp": "0.34.2"
|
"sharp": "0.34.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.57.0",
|
||||||
"@testing-library/react": "16.3.0",
|
"@testing-library/react": "16.3.0",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.10.2",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.2.7",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.2.3",
|
||||||
"@vitejs/plugin-react": "4.5.2",
|
"@vitejs/plugin-react": "4.5.2",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "4.0.15",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-next": "15.4.7",
|
"eslint-config-next": "15.5.9",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"playwright": "1.56.1",
|
"playwright": "1.57.0",
|
||||||
"playwright-core": "1.56.1",
|
"playwright-core": "1.57.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "5.7.3",
|
"typescript": "5.9.3",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "6.0.0",
|
||||||
"vitest": "3.2.4"
|
"vitest": "4.0.15"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.20.2 || >=20.9.0",
|
"node": "^18.20.2 || >=20.9.0",
|
||||||
|
|
|
||||||
1647
pnpm-lock.yaml
1647
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
25
scripts/check-framework-updates.sh
Executable file
25
scripts/check-framework-updates.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! command -v pnpm >/dev/null 2>&1; then
|
||||||
|
echo "pnpm is required to run this check." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔍 Checking Payload + Next.js versions (peer compatibility)…"
|
||||||
|
pnpm outdated next \
|
||||||
|
payload \
|
||||||
|
@payloadcms/next \
|
||||||
|
@payloadcms/db-postgres \
|
||||||
|
@payloadcms/plugin-form-builder \
|
||||||
|
@payloadcms/plugin-multi-tenant \
|
||||||
|
@payloadcms/plugin-nested-docs \
|
||||||
|
@payloadcms/plugin-redirects \
|
||||||
|
@payloadcms/plugin-seo \
|
||||||
|
@payloadcms/richtext-lexical \
|
||||||
|
@payloadcms/ui
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "ℹ️ Review Payload release notes: https://github.com/payloadcms/payload/releases"
|
||||||
|
echo "ℹ️ Review Next.js release notes: https://nextjs.org/blog"
|
||||||
|
|
@ -19,19 +19,6 @@ const TIMELINE_RATE_LIMIT = 30
|
||||||
const TIMELINE_TYPES = ['history', 'milestones', 'releases', 'career', 'events', 'process'] as const
|
const TIMELINE_TYPES = ['history', 'milestones', 'releases', 'career', 'events', 'process'] as const
|
||||||
type TimelineType = (typeof TIMELINE_TYPES)[number]
|
type TimelineType = (typeof TIMELINE_TYPES)[number]
|
||||||
|
|
||||||
// Event category for filtering
|
|
||||||
const EVENT_CATEGORIES = [
|
|
||||||
'milestone',
|
|
||||||
'founding',
|
|
||||||
'product',
|
|
||||||
'team',
|
|
||||||
'award',
|
|
||||||
'partnership',
|
|
||||||
'expansion',
|
|
||||||
'technology',
|
|
||||||
'other',
|
|
||||||
] as const
|
|
||||||
|
|
||||||
interface TimelineEvent {
|
interface TimelineEvent {
|
||||||
dateType: string
|
dateType: string
|
||||||
year?: number
|
year?: number
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,6 @@ const WORKFLOW_TYPES = [
|
||||||
] as const
|
] as const
|
||||||
type WorkflowType = (typeof WORKFLOW_TYPES)[number]
|
type WorkflowType = (typeof WORKFLOW_TYPES)[number]
|
||||||
|
|
||||||
// Step types for filtering
|
|
||||||
const STEP_TYPES = ['task', 'decision', 'milestone', 'approval', 'wait', 'automatic'] as const
|
|
||||||
|
|
||||||
// Valid complexity values (must match Workflows.ts select options)
|
// Valid complexity values (must match Workflows.ts select options)
|
||||||
const COMPLEXITY_VALUES = ['simple', 'medium', 'complex', 'very_complex'] as const
|
const COMPLEXITY_VALUES = ['simple', 'medium', 'complex', 'very_complex'] as const
|
||||||
type ComplexityValue = (typeof COMPLEXITY_VALUES)[number]
|
type ComplexityValue = (typeof COMPLEXITY_VALUES)[number]
|
||||||
|
|
|
||||||
|
|
@ -169,10 +169,10 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logs abrufen - Type assertion für where da email-logs noch nicht in payload-types
|
type FindArgs = Parameters<typeof payload.find>[0]
|
||||||
const result = await (payload.find as Function)({
|
const result = await payload.find({
|
||||||
collection: 'email-logs',
|
collection: 'email-logs',
|
||||||
where,
|
where: where as FindArgs['where'],
|
||||||
limit,
|
limit,
|
||||||
sort: '-createdAt',
|
sort: '-createdAt',
|
||||||
depth: 1,
|
depth: 1,
|
||||||
|
|
|
||||||
|
|
@ -97,33 +97,29 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
baseWhere.tenant = { in: tenantFilter }
|
baseWhere.tenant = { in: tenantFilter }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistiken parallel abrufen - Type assertions für email-logs Collection
|
|
||||||
const countFn = payload.count as Function
|
|
||||||
const findFn = payload.find as Function
|
|
||||||
|
|
||||||
const [totalResult, sentResult, failedResult, pendingResult, recentFailed] = await Promise.all([
|
const [totalResult, sentResult, failedResult, pendingResult, recentFailed] = await Promise.all([
|
||||||
// Gesamt
|
// Gesamt
|
||||||
countFn({
|
payload.count({
|
||||||
collection: 'email-logs',
|
collection: 'email-logs',
|
||||||
where: baseWhere,
|
where: baseWhere,
|
||||||
}),
|
}),
|
||||||
// Gesendet
|
// Gesendet
|
||||||
countFn({
|
payload.count({
|
||||||
collection: 'email-logs',
|
collection: 'email-logs',
|
||||||
where: { ...baseWhere, status: { equals: 'sent' } },
|
where: { ...baseWhere, status: { equals: 'sent' } },
|
||||||
}),
|
}),
|
||||||
// Fehlgeschlagen
|
// Fehlgeschlagen
|
||||||
countFn({
|
payload.count({
|
||||||
collection: 'email-logs',
|
collection: 'email-logs',
|
||||||
where: { ...baseWhere, status: { equals: 'failed' } },
|
where: { ...baseWhere, status: { equals: 'failed' } },
|
||||||
}),
|
}),
|
||||||
// Ausstehend
|
// Ausstehend
|
||||||
countFn({
|
payload.count({
|
||||||
collection: 'email-logs',
|
collection: 'email-logs',
|
||||||
where: { ...baseWhere, status: { equals: 'pending' } },
|
where: { ...baseWhere, status: { equals: 'pending' } },
|
||||||
}),
|
}),
|
||||||
// Letzte 5 fehlgeschlagene (für Quick-View)
|
// Letzte 5 fehlgeschlagene (für Quick-View)
|
||||||
findFn({
|
payload.find({
|
||||||
collection: 'email-logs',
|
collection: 'email-logs',
|
||||||
where: { ...baseWhere, status: { equals: 'failed' } },
|
where: { ...baseWhere, status: { equals: 'failed' } },
|
||||||
limit: 5,
|
limit: 5,
|
||||||
|
|
@ -145,7 +141,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
sources.map(async (source) => {
|
sources.map(async (source) => {
|
||||||
const result = await countFn({
|
const result = await payload.count({
|
||||||
collection: 'email-logs',
|
collection: 'email-logs',
|
||||||
where: { ...baseWhere, source: { equals: source } },
|
where: { ...baseWhere, source: { equals: source } },
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import { getPayload } from 'payload'
|
import { getPayload } from 'payload'
|
||||||
import config from '@payload-config'
|
import config from '@payload-config'
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { enqueuePdf, getPdfJobStatus, getPdfJobResult, isQueueAvailable } from '@/lib/queue'
|
import { enqueuePdf, getPdfJobStatus, isQueueAvailable } from '@/lib/queue'
|
||||||
import { generatePdfFromHtml, generatePdfFromUrl } from '@/lib/pdf/pdf-service'
|
import { generatePdfFromHtml, generatePdfFromUrl } from '@/lib/pdf/pdf-service'
|
||||||
import { logAccessDenied } from '@/lib/audit/audit-service'
|
import { logAccessDenied } from '@/lib/audit/audit-service'
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,4 @@
|
||||||
import configPromise from '@payload-config'
|
export const GET = async () => {
|
||||||
import { getPayload } from 'payload'
|
|
||||||
|
|
||||||
export const GET = async (request: Request) => {
|
|
||||||
const payload = await getPayload({
|
|
||||||
config: configPromise,
|
|
||||||
})
|
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
message: 'This is an example of a custom route.',
|
message: 'This is an example of a custom route.',
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -420,7 +420,7 @@ export const Bookings: CollectionConfig = {
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeChange: [
|
beforeChange: [
|
||||||
({ data, req, operation }) => {
|
({ data, req }) => {
|
||||||
// Auto-set author for new notes
|
// Auto-set author for new notes
|
||||||
if (data?.internalNotes && req.user) {
|
if (data?.internalNotes && req.user) {
|
||||||
data.internalNotes = data.internalNotes.map((note: Record<string, unknown>) => {
|
data.internalNotes = data.internalNotes.map((note: Record<string, unknown>) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// src/collections/ConsentLogs.ts
|
// src/collections/ConsentLogs.ts
|
||||||
|
|
||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig, PayloadRequest } from 'payload'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { env } from '../lib/envValidation'
|
import { env } from '../lib/envValidation'
|
||||||
import { authenticatedOnly } from '../lib/tenantAccess'
|
import { authenticatedOnly } from '../lib/tenantAccess'
|
||||||
|
|
@ -30,24 +30,21 @@ function anonymizeIp(ip: string, tenantId: string): string {
|
||||||
* Extrahiert die Client-IP aus dem Request.
|
* Extrahiert die Client-IP aus dem Request.
|
||||||
* Berücksichtigt Reverse-Proxy-Header.
|
* Berücksichtigt Reverse-Proxy-Header.
|
||||||
*/
|
*/
|
||||||
function extractClientIp(req: any): string {
|
function extractClientIp(req: PayloadRequest): string {
|
||||||
// X-Forwarded-For kann mehrere IPs enthalten (Client, Proxies)
|
// X-Forwarded-For kann mehrere IPs enthalten (Client, Proxies)
|
||||||
const forwarded = req.headers?.['x-forwarded-for']
|
const forwarded = req.headers?.get?.('x-forwarded-for')
|
||||||
if (typeof forwarded === 'string') {
|
if (typeof forwarded === 'string') {
|
||||||
return forwarded.split(',')[0].trim()
|
return forwarded.split(',')[0].trim()
|
||||||
}
|
}
|
||||||
if (Array.isArray(forwarded) && forwarded.length > 0) {
|
|
||||||
return String(forwarded[0]).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
// X-Real-IP (einzelne IP)
|
// X-Real-IP (einzelne IP)
|
||||||
const realIp = req.headers?.['x-real-ip']
|
const realIp = req.headers?.get?.('x-real-ip')
|
||||||
if (typeof realIp === 'string') {
|
if (typeof realIp === 'string') {
|
||||||
return realIp.trim()
|
return realIp.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Socket Remote Address
|
// Fallback: unknown (PayloadRequest hat keinen direkten IP-Zugriff mehr)
|
||||||
return req.socket?.remoteAddress || req.ip || 'unknown'
|
return 'unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,6 @@ import { logEmailFailed } from '../lib/audit/audit-service'
|
||||||
const failedEmailCounter: Map<number, { count: number; lastReset: number }> = new Map()
|
const failedEmailCounter: Map<number, { count: number; lastReset: number }> = new Map()
|
||||||
const RESET_INTERVAL = 60 * 60 * 1000 // 1 Stunde
|
const RESET_INTERVAL = 60 * 60 * 1000 // 1 Stunde
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt die Anzahl der fehlgeschlagenen E-Mails für einen Tenant zurück
|
|
||||||
*/
|
|
||||||
function getFailedCount(tenantId: number): number {
|
|
||||||
const now = Date.now()
|
|
||||||
const entry = failedEmailCounter.get(tenantId)
|
|
||||||
|
|
||||||
if (!entry || now - entry.lastReset > RESET_INTERVAL) {
|
|
||||||
failedEmailCounter.set(tenantId, { count: 0, lastReset: now })
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry.count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inkrementiert den Zähler für fehlgeschlagene E-Mails
|
* Inkrementiert den Zähler für fehlgeschlagene E-Mails
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
// src/hooks/formSubmissionHooks.ts
|
// src/hooks/formSubmissionHooks.ts
|
||||||
|
|
||||||
import type {
|
import type { CollectionBeforeChangeHook } from 'payload'
|
||||||
CollectionBeforeChangeHook,
|
|
||||||
CollectionAfterReadHook,
|
|
||||||
FieldHook,
|
|
||||||
} from 'payload'
|
|
||||||
|
|
||||||
interface InternalNote {
|
interface InternalNote {
|
||||||
note: string
|
note: string
|
||||||
|
|
@ -12,12 +8,21 @@ interface InternalNote {
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ResponseTracking {
|
||||||
|
responded?: boolean
|
||||||
|
respondedAt?: string
|
||||||
|
respondedBy?: number | string | { id: number | string }
|
||||||
|
method?: string
|
||||||
|
summary?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface FormSubmissionDoc {
|
interface FormSubmissionDoc {
|
||||||
id: number | string
|
id: number | string
|
||||||
status?: string
|
status?: string
|
||||||
readAt?: string
|
readAt?: string
|
||||||
readBy?: number | string | { id: number | string }
|
readBy?: number | string | { id: number | string }
|
||||||
internalNotes?: InternalNote[]
|
internalNotes?: InternalNote[]
|
||||||
|
responseTracking?: ResponseTracking
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +103,7 @@ export const setResponseTimestamp: CollectionBeforeChangeHook<FormSubmissionDoc>
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
responseTracking: {
|
responseTracking: {
|
||||||
...data.responseTracking,
|
...(data.responseTracking || {}),
|
||||||
respondedAt: new Date().toISOString(),
|
respondedAt: new Date().toISOString(),
|
||||||
respondedBy: req.user.id,
|
respondedBy: req.user.id,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -154,8 +154,8 @@ export async function createAuditLog(
|
||||||
const maskedNewValue = input.newValue ? maskObject(input.newValue) : undefined
|
const maskedNewValue = input.newValue ? maskObject(input.newValue) : undefined
|
||||||
const maskedMetadata = input.metadata ? maskObject(input.metadata) : undefined
|
const maskedMetadata = input.metadata ? maskObject(input.metadata) : undefined
|
||||||
|
|
||||||
// Type assertion notwendig bis payload-types.ts regeneriert wird
|
type CreateArgs = Parameters<typeof payload.create>[0]
|
||||||
await (payload.create as Function)({
|
await payload.create({
|
||||||
collection: 'audit-logs',
|
collection: 'audit-logs',
|
||||||
data: {
|
data: {
|
||||||
action: input.action,
|
action: input.action,
|
||||||
|
|
@ -174,7 +174,7 @@ export async function createAuditLog(
|
||||||
},
|
},
|
||||||
// Bypass Access Control für System-Logging
|
// Bypass Access Control für System-Logging
|
||||||
overrideAccess: true,
|
overrideAccess: true,
|
||||||
})
|
} as CreateArgs)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fehler beim Audit-Logging sollten die Hauptoperation nicht blockieren
|
// Fehler beim Audit-Logging sollten die Hauptoperation nicht blockieren
|
||||||
// Auch Fehlermeldungen maskieren
|
// Auch Fehlermeldungen maskieren
|
||||||
|
|
@ -473,13 +473,5 @@ function maskSensitiveData(text: string): string {
|
||||||
return maskString(text)
|
return maskString(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Maskiert Objekte für Audit-Logs (previousValue, newValue, metadata)
|
|
||||||
*/
|
|
||||||
function maskAuditData(data: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
||||||
if (!data) return undefined
|
|
||||||
return maskObject(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-export für externe Nutzung
|
// Re-export für externe Nutzung
|
||||||
export { maskError, maskObject, maskString }
|
export { maskError, maskObject, maskString }
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,6 @@ export const localeNames: Record<Locale, { native: string; english: string }> =
|
||||||
* Get locale direction (for RTL support in future)
|
* Get locale direction (for RTL support in future)
|
||||||
*/
|
*/
|
||||||
export function getLocaleDirection(locale: Locale): 'ltr' | 'rtl' {
|
export function getLocaleDirection(locale: Locale): 'ltr' | 'rtl' {
|
||||||
// Both German and English are LTR
|
const rtlLocales: Locale[] = []
|
||||||
return 'ltr'
|
return rtlLocales.includes(locale) ? 'rtl' : 'ltr'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ export function startEmailWorker(): Worker<EmailJobData, EmailJobResult> {
|
||||||
console.log(`[EmailWorker] Ready (concurrency: ${CONCURRENCY})`)
|
console.log(`[EmailWorker] Ready (concurrency: ${CONCURRENCY})`)
|
||||||
})
|
})
|
||||||
|
|
||||||
emailWorker.on('completed', (job, result) => {
|
emailWorker.on('completed', (job) => {
|
||||||
console.log(`[EmailWorker] Job ${job.id} completed in ${Date.now() - job.timestamp}ms`)
|
console.log(`[EmailWorker] Job ${job.id} completed in ${Date.now() - job.timestamp}ms`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ async function processPdfJob(job: Job<PdfJobData>): Promise<PdfJobResult> {
|
||||||
options = {},
|
options = {},
|
||||||
tenantId,
|
tenantId,
|
||||||
documentType,
|
documentType,
|
||||||
correlationId,
|
|
||||||
} = job.data
|
} = job.data
|
||||||
|
|
||||||
console.log(`[PdfWorker] Processing job ${job.id} for tenant ${tenantId} (source: ${source})`)
|
console.log(`[PdfWorker] Processing job ${job.id} for tenant ${tenantId} (source: ${source})`)
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,9 @@ export interface Config {
|
||||||
jobs: Job;
|
jobs: Job;
|
||||||
downloads: Download;
|
downloads: Download;
|
||||||
events: Event;
|
events: Event;
|
||||||
|
bookings: Booking;
|
||||||
|
certifications: Certification;
|
||||||
|
projects: Project;
|
||||||
'cookie-configurations': CookieConfiguration;
|
'cookie-configurations': CookieConfiguration;
|
||||||
'cookie-inventory': CookieInventory;
|
'cookie-inventory': CookieInventory;
|
||||||
'consent-logs': ConsentLog;
|
'consent-logs': ConsentLog;
|
||||||
|
|
@ -135,6 +138,9 @@ export interface Config {
|
||||||
jobs: JobsSelect<false> | JobsSelect<true>;
|
jobs: JobsSelect<false> | JobsSelect<true>;
|
||||||
downloads: DownloadsSelect<false> | DownloadsSelect<true>;
|
downloads: DownloadsSelect<false> | DownloadsSelect<true>;
|
||||||
events: EventsSelect<false> | EventsSelect<true>;
|
events: EventsSelect<false> | EventsSelect<true>;
|
||||||
|
bookings: BookingsSelect<false> | BookingsSelect<true>;
|
||||||
|
certifications: CertificationsSelect<false> | CertificationsSelect<true>;
|
||||||
|
projects: ProjectsSelect<false> | ProjectsSelect<true>;
|
||||||
'cookie-configurations': CookieConfigurationsSelect<false> | CookieConfigurationsSelect<true>;
|
'cookie-configurations': CookieConfigurationsSelect<false> | CookieConfigurationsSelect<true>;
|
||||||
'cookie-inventory': CookieInventorySelect<false> | CookieInventorySelect<true>;
|
'cookie-inventory': CookieInventorySelect<false> | CookieInventorySelect<true>;
|
||||||
'consent-logs': ConsentLogsSelect<false> | ConsentLogsSelect<true>;
|
'consent-logs': ConsentLogsSelect<false> | ConsentLogsSelect<true>;
|
||||||
|
|
@ -2584,6 +2590,90 @@ export interface Page {
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'comparison';
|
blockType: 'comparison';
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
title?: string | null;
|
||||||
|
subtitle?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
comparisons: {
|
||||||
|
title?: string | null;
|
||||||
|
beforeImage: number | Media;
|
||||||
|
afterImage: number | Media;
|
||||||
|
beforeLabel?: string | null;
|
||||||
|
afterLabel?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
category?:
|
||||||
|
| (
|
||||||
|
| 'wedding'
|
||||||
|
| 'portrait'
|
||||||
|
| 'retouch'
|
||||||
|
| 'colorgrade'
|
||||||
|
| 'restore'
|
||||||
|
| 'composing'
|
||||||
|
| 'architecture'
|
||||||
|
| 'product'
|
||||||
|
| 'other'
|
||||||
|
)
|
||||||
|
| null;
|
||||||
|
/**
|
||||||
|
* Komma-getrennte Tags für Filterung
|
||||||
|
*/
|
||||||
|
tags?: string | null;
|
||||||
|
metadata?: {
|
||||||
|
client?: string | null;
|
||||||
|
date?: string | null;
|
||||||
|
/**
|
||||||
|
* z.B. Lightroom, Photoshop
|
||||||
|
*/
|
||||||
|
tools?: string | null;
|
||||||
|
duration?: string | null;
|
||||||
|
};
|
||||||
|
showMetadata?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[];
|
||||||
|
displayStyle?: ('slider' | 'hover' | 'toggle' | 'side-by-side' | 'fade') | null;
|
||||||
|
sliderOrientation?: ('horizontal' | 'vertical') | null;
|
||||||
|
/**
|
||||||
|
* 0 = links/oben, 100 = rechts/unten
|
||||||
|
*/
|
||||||
|
sliderStartPosition?: number | null;
|
||||||
|
layout?: ('single' | 'grid-2' | 'grid-3' | 'carousel' | 'masonry') | null;
|
||||||
|
aspectRatio?: ('original' | '16-9' | '4-3' | '3-2' | '1-1' | '2-3' | '9-16') | null;
|
||||||
|
sliderHandle?: {
|
||||||
|
style?: ('circle' | 'line' | 'arrows' | 'custom') | null;
|
||||||
|
color?: ('white' | 'black' | 'primary' | 'accent') | null;
|
||||||
|
size?: ('small' | 'medium' | 'large') | null;
|
||||||
|
showLine?: boolean | null;
|
||||||
|
};
|
||||||
|
showLabels?: boolean | null;
|
||||||
|
labelPosition?: ('corners' | 'top' | 'bottom' | 'overlay') | null;
|
||||||
|
labelStyle?: ('badge' | 'text' | 'pill') | null;
|
||||||
|
showFilter?: boolean | null;
|
||||||
|
animation?: {
|
||||||
|
enableAnimation?: boolean | null;
|
||||||
|
autoPlay?: boolean | null;
|
||||||
|
autoPlaySpeed?: number | null;
|
||||||
|
scrollTrigger?: boolean | null;
|
||||||
|
};
|
||||||
|
interactivity?: {
|
||||||
|
enableZoom?: boolean | null;
|
||||||
|
enableFullscreen?: boolean | null;
|
||||||
|
enableSwipe?: boolean | null;
|
||||||
|
enableKeyboard?: boolean | null;
|
||||||
|
};
|
||||||
|
cta?: {
|
||||||
|
showCta?: boolean | null;
|
||||||
|
ctaText?: string | null;
|
||||||
|
ctaLink?: string | null;
|
||||||
|
ctaStyle?: ('primary' | 'secondary' | 'outline' | 'ghost') | null;
|
||||||
|
};
|
||||||
|
backgroundColor?: ('transparent' | 'white' | 'light' | 'dark' | 'black') | null;
|
||||||
|
borderRadius?: ('none' | 'small' | 'medium' | 'large') | null;
|
||||||
|
shadow?: ('none' | 'small' | 'medium' | 'large') | null;
|
||||||
|
spacing?: ('none' | 'small' | 'medium' | 'large') | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'before-after';
|
||||||
|
}
|
||||||
)[]
|
)[]
|
||||||
| null;
|
| null;
|
||||||
seo?: {
|
seo?: {
|
||||||
|
|
@ -4941,6 +5031,429 @@ export interface Workflow {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Terminbuchungen für Fotoshootings
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "bookings".
|
||||||
|
*/
|
||||||
|
export interface Booking {
|
||||||
|
id: number;
|
||||||
|
tenant?: (number | null) | Tenant;
|
||||||
|
customerName: string;
|
||||||
|
customerEmail: string;
|
||||||
|
customerPhone?: string | null;
|
||||||
|
customerCompany?: string | null;
|
||||||
|
serviceType:
|
||||||
|
| 'wedding'
|
||||||
|
| 'portrait'
|
||||||
|
| 'business'
|
||||||
|
| 'event'
|
||||||
|
| 'product'
|
||||||
|
| 'family'
|
||||||
|
| 'newborn'
|
||||||
|
| 'maternity'
|
||||||
|
| 'realestate'
|
||||||
|
| 'other';
|
||||||
|
/**
|
||||||
|
* Optional: Verknüpfung mit einem definierten Service
|
||||||
|
*/
|
||||||
|
service?: (number | null) | Service;
|
||||||
|
date: string;
|
||||||
|
/**
|
||||||
|
* z.B. 14:00 Uhr
|
||||||
|
*/
|
||||||
|
time?: string | null;
|
||||||
|
duration?: ('30' | '60' | '120' | '180' | '240' | '480' | 'custom') | null;
|
||||||
|
locationType?: ('studio' | 'outdoor' | 'customer' | 'event' | 'tbd') | null;
|
||||||
|
locationAddress?: string | null;
|
||||||
|
/**
|
||||||
|
* Wie viele Personen sollen fotografiert werden?
|
||||||
|
*/
|
||||||
|
participants?: number | null;
|
||||||
|
/**
|
||||||
|
* Besondere Wünsche, Ideen oder Anmerkungen
|
||||||
|
*/
|
||||||
|
message?: string | null;
|
||||||
|
/**
|
||||||
|
* Beispielbilder für gewünschten Stil
|
||||||
|
*/
|
||||||
|
referenceImages?:
|
||||||
|
| {
|
||||||
|
image?: (number | null) | Media;
|
||||||
|
note?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
status: 'pending' | 'review' | 'confirmed' | 'deposit' | 'completed' | 'cancelled' | 'noshow';
|
||||||
|
priority?: ('high' | 'normal' | 'low') | null;
|
||||||
|
pricing?: {
|
||||||
|
/**
|
||||||
|
* In Euro
|
||||||
|
*/
|
||||||
|
estimatedPrice?: number | null;
|
||||||
|
finalPrice?: number | null;
|
||||||
|
depositAmount?: number | null;
|
||||||
|
depositPaid?: boolean | null;
|
||||||
|
fullyPaid?: boolean | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Nur für interne Verwendung
|
||||||
|
*/
|
||||||
|
internalNotes?:
|
||||||
|
| {
|
||||||
|
note: string;
|
||||||
|
author?: (number | null) | User;
|
||||||
|
createdAt?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
contactHistory?:
|
||||||
|
| {
|
||||||
|
type: 'email_sent' | 'email_received' | 'call' | 'whatsapp' | 'inperson';
|
||||||
|
summary: string;
|
||||||
|
date?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
assignedTo?: (number | null) | User;
|
||||||
|
source?: ('website' | 'phone' | 'email' | 'instagram' | 'facebook' | 'referral' | 'returning' | 'other') | null;
|
||||||
|
/**
|
||||||
|
* Kunde hat Datenschutzerklärung akzeptiert
|
||||||
|
*/
|
||||||
|
gdprConsent?: boolean | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Zertifizierungen, Akkreditierungen und Qualitätssiegel
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "certifications".
|
||||||
|
*/
|
||||||
|
export interface Certification {
|
||||||
|
id: number;
|
||||||
|
tenant?: (number | null) | Tenant;
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* URL-freundlicher Name
|
||||||
|
*/
|
||||||
|
slug: string;
|
||||||
|
description?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Für Übersichten und Meta-Beschreibungen
|
||||||
|
*/
|
||||||
|
shortDescription?: string | null;
|
||||||
|
type: 'iso' | 'din' | 'accreditation' | 'seal' | 'membership' | 'award' | 'license' | 'approval' | 'other';
|
||||||
|
category?:
|
||||||
|
| (
|
||||||
|
| 'quality'
|
||||||
|
| 'care'
|
||||||
|
| 'medical'
|
||||||
|
| 'hygiene'
|
||||||
|
| 'safety'
|
||||||
|
| 'privacy'
|
||||||
|
| 'environment'
|
||||||
|
| 'hr'
|
||||||
|
| 'it-security'
|
||||||
|
| 'accessibility'
|
||||||
|
| 'other'
|
||||||
|
)
|
||||||
|
| null;
|
||||||
|
issuer: {
|
||||||
|
name: string;
|
||||||
|
logo?: (number | null) | Media;
|
||||||
|
website?: string | null;
|
||||||
|
country?: ('DE' | 'AT' | 'CH' | 'EU' | 'INT') | null;
|
||||||
|
};
|
||||||
|
certNumber?: string | null;
|
||||||
|
issuedDate?: string | null;
|
||||||
|
validUntil?: string | null;
|
||||||
|
renewalCycle?: ('yearly' | '2years' | '3years' | '5years' | 'unlimited') | null;
|
||||||
|
logo?: (number | null) | Media;
|
||||||
|
/**
|
||||||
|
* Das offizielle Zertifikatsdokument
|
||||||
|
*/
|
||||||
|
certificate?: (number | null) | Media;
|
||||||
|
gallery?:
|
||||||
|
| {
|
||||||
|
document?: (number | null) | Media;
|
||||||
|
title?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
scope?: {
|
||||||
|
description?: string | null;
|
||||||
|
/**
|
||||||
|
* Für welche Standorte gilt die Zertifizierung?
|
||||||
|
*/
|
||||||
|
locations?: (number | Location)[] | null;
|
||||||
|
/**
|
||||||
|
* Für welche Leistungen gilt die Zertifizierung?
|
||||||
|
*/
|
||||||
|
services?: (number | Service)[] | null;
|
||||||
|
};
|
||||||
|
requirements?:
|
||||||
|
| {
|
||||||
|
requirement: string;
|
||||||
|
description?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
benefits?:
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
description?: string | null;
|
||||||
|
icon?: ('check' | 'star' | 'shield' | 'heart' | 'lock' | 'search' | 'clock' | 'document') | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
/**
|
||||||
|
* Historie der durchgeführten Audits
|
||||||
|
*/
|
||||||
|
audits?:
|
||||||
|
| {
|
||||||
|
date: string;
|
||||||
|
type?: ('initial' | 'surveillance' | 'recertification' | 'special') | null;
|
||||||
|
result?: ('passed' | 'conditional' | 'failed') | null;
|
||||||
|
notes?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
status: 'active' | 'pending' | 'renewal' | 'suspended' | 'expired' | 'withdrawn';
|
||||||
|
visibility?: ('public' | 'request' | 'internal') | null;
|
||||||
|
/**
|
||||||
|
* Höhere Zahl = höhere Priorität in der Anzeige
|
||||||
|
*/
|
||||||
|
priority?: number | null;
|
||||||
|
showOnHomepage?: boolean | null;
|
||||||
|
seo?: {
|
||||||
|
metaTitle?: string | null;
|
||||||
|
metaDescription?: string | null;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Projekte, Spiele und kreative Arbeiten
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "projects".
|
||||||
|
*/
|
||||||
|
export interface Project {
|
||||||
|
id: number;
|
||||||
|
tenant?: (number | null) | Tenant;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
/**
|
||||||
|
* Kurze, prägnante Beschreibung (1 Zeile)
|
||||||
|
*/
|
||||||
|
tagline?: string | null;
|
||||||
|
description?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Für Übersichten und Social Media
|
||||||
|
*/
|
||||||
|
shortDescription?: string | null;
|
||||||
|
type: 'game' | 'demo' | 'mod' | 'tool' | 'assets' | 'prototype' | 'gamejam' | 'tutorial' | 'opensource' | 'other';
|
||||||
|
genres?:
|
||||||
|
| (
|
||||||
|
| 'action'
|
||||||
|
| 'adventure'
|
||||||
|
| 'rpg'
|
||||||
|
| 'strategy'
|
||||||
|
| 'simulation'
|
||||||
|
| 'puzzle'
|
||||||
|
| 'horror'
|
||||||
|
| 'shooter'
|
||||||
|
| 'platformer'
|
||||||
|
| 'racing'
|
||||||
|
| 'sports'
|
||||||
|
| 'fighting'
|
||||||
|
| 'music'
|
||||||
|
| 'visualnovel'
|
||||||
|
| 'survival'
|
||||||
|
| 'sandbox'
|
||||||
|
| 'towerdefense'
|
||||||
|
| 'roguelike'
|
||||||
|
| 'indie'
|
||||||
|
)[]
|
||||||
|
| null;
|
||||||
|
platforms?:
|
||||||
|
| (
|
||||||
|
| 'windows'
|
||||||
|
| 'macos'
|
||||||
|
| 'linux'
|
||||||
|
| 'web'
|
||||||
|
| 'ios'
|
||||||
|
| 'android'
|
||||||
|
| 'playstation'
|
||||||
|
| 'xbox'
|
||||||
|
| 'switch'
|
||||||
|
| 'steamdeck'
|
||||||
|
| 'vr'
|
||||||
|
)[]
|
||||||
|
| null;
|
||||||
|
featuredImage: number | Media;
|
||||||
|
logo?: (number | null) | Media;
|
||||||
|
screenshots?:
|
||||||
|
| {
|
||||||
|
image: number | Media;
|
||||||
|
caption?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
videos?:
|
||||||
|
| {
|
||||||
|
type: 'trailer' | 'gameplay' | 'devlog' | 'tutorial' | 'other';
|
||||||
|
title?: string | null;
|
||||||
|
/**
|
||||||
|
* YouTube, Vimeo oder direkter Link
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
thumbnail?: (number | null) | Media;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
techStack?: {
|
||||||
|
engine?:
|
||||||
|
| (
|
||||||
|
| 'unity'
|
||||||
|
| 'unreal'
|
||||||
|
| 'godot'
|
||||||
|
| 'gamemaker'
|
||||||
|
| 'rpgmaker'
|
||||||
|
| 'construct'
|
||||||
|
| 'custom'
|
||||||
|
| 'renpy'
|
||||||
|
| 'phaser'
|
||||||
|
| 'other'
|
||||||
|
)
|
||||||
|
| null;
|
||||||
|
languages?:
|
||||||
|
| ('csharp' | 'cpp' | 'gdscript' | 'javascript' | 'typescript' | 'python' | 'lua' | 'rust' | 'blueprint')[]
|
||||||
|
| null;
|
||||||
|
/**
|
||||||
|
* z.B. Blender, Aseprite, FMOD
|
||||||
|
*/
|
||||||
|
tools?: string | null;
|
||||||
|
};
|
||||||
|
requirements?: {
|
||||||
|
minimum?: {
|
||||||
|
os?: string | null;
|
||||||
|
cpu?: string | null;
|
||||||
|
ram?: string | null;
|
||||||
|
gpu?: string | null;
|
||||||
|
storage?: string | null;
|
||||||
|
};
|
||||||
|
recommended?: {
|
||||||
|
os?: string | null;
|
||||||
|
cpu?: string | null;
|
||||||
|
ram?: string | null;
|
||||||
|
gpu?: string | null;
|
||||||
|
storage?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
releaseDate?: string | null;
|
||||||
|
links?: {
|
||||||
|
website?: string | null;
|
||||||
|
steam?: string | null;
|
||||||
|
itchio?: string | null;
|
||||||
|
epicGames?: string | null;
|
||||||
|
gog?: string | null;
|
||||||
|
playStore?: string | null;
|
||||||
|
appStore?: string | null;
|
||||||
|
github?: string | null;
|
||||||
|
discord?: string | null;
|
||||||
|
twitter?: string | null;
|
||||||
|
};
|
||||||
|
downloads?:
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* z.B. "Windows Build", "Demo v0.5"
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
platform?: ('windows' | 'macos' | 'linux' | 'universal') | null;
|
||||||
|
version?: string | null;
|
||||||
|
file?: (number | null) | Media;
|
||||||
|
externalUrl?: string | null;
|
||||||
|
size?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
features?:
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
description?: string | null;
|
||||||
|
icon?: (number | null) | Media;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
team?:
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
role?: string | null;
|
||||||
|
link?: string | null;
|
||||||
|
avatar?: (number | null) | Media;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
gameJam?: {
|
||||||
|
jamName?: string | null;
|
||||||
|
theme?: string | null;
|
||||||
|
/**
|
||||||
|
* z.B. "48 Stunden"
|
||||||
|
*/
|
||||||
|
duration?: string | null;
|
||||||
|
ranking?: string | null;
|
||||||
|
jamLink?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Verknüpfte Blog-Posts über dieses Projekt
|
||||||
|
*/
|
||||||
|
devlogs?: (number | Post)[] | null;
|
||||||
|
status: 'development' | 'earlyaccess' | 'released' | 'paused' | 'cancelled' | 'completed';
|
||||||
|
visibility?: ('public' | 'draft' | 'unlisted' | 'private') | null;
|
||||||
|
featured?: boolean | null;
|
||||||
|
/**
|
||||||
|
* Höher = weiter oben
|
||||||
|
*/
|
||||||
|
sortOrder?: number | null;
|
||||||
|
seo?: {
|
||||||
|
metaTitle?: string | null;
|
||||||
|
metaDescription?: string | null;
|
||||||
|
ogImage?: (number | null) | Media;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Cookie-Banner Konfiguration pro Tenant
|
* Cookie-Banner Konfiguration pro Tenant
|
||||||
*
|
*
|
||||||
|
|
@ -5469,6 +5982,18 @@ export interface PayloadLockedDocument {
|
||||||
relationTo: 'events';
|
relationTo: 'events';
|
||||||
value: number | Event;
|
value: number | Event;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'bookings';
|
||||||
|
value: number | Booking;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'certifications';
|
||||||
|
value: number | Certification;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'projects';
|
||||||
|
value: number | Project;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'cookie-configurations';
|
relationTo: 'cookie-configurations';
|
||||||
value: number | CookieConfiguration;
|
value: number | CookieConfiguration;
|
||||||
|
|
@ -7442,6 +7967,82 @@ export interface PagesSelect<T extends boolean = true> {
|
||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
};
|
};
|
||||||
|
'before-after'?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
subtitle?: T;
|
||||||
|
description?: T;
|
||||||
|
comparisons?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
beforeImage?: T;
|
||||||
|
afterImage?: T;
|
||||||
|
beforeLabel?: T;
|
||||||
|
afterLabel?: T;
|
||||||
|
description?: T;
|
||||||
|
category?: T;
|
||||||
|
tags?: T;
|
||||||
|
metadata?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
client?: T;
|
||||||
|
date?: T;
|
||||||
|
tools?: T;
|
||||||
|
duration?: T;
|
||||||
|
};
|
||||||
|
showMetadata?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
displayStyle?: T;
|
||||||
|
sliderOrientation?: T;
|
||||||
|
sliderStartPosition?: T;
|
||||||
|
layout?: T;
|
||||||
|
aspectRatio?: T;
|
||||||
|
sliderHandle?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
style?: T;
|
||||||
|
color?: T;
|
||||||
|
size?: T;
|
||||||
|
showLine?: T;
|
||||||
|
};
|
||||||
|
showLabels?: T;
|
||||||
|
labelPosition?: T;
|
||||||
|
labelStyle?: T;
|
||||||
|
showFilter?: T;
|
||||||
|
animation?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
enableAnimation?: T;
|
||||||
|
autoPlay?: T;
|
||||||
|
autoPlaySpeed?: T;
|
||||||
|
scrollTrigger?: T;
|
||||||
|
};
|
||||||
|
interactivity?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
enableZoom?: T;
|
||||||
|
enableFullscreen?: T;
|
||||||
|
enableSwipe?: T;
|
||||||
|
enableKeyboard?: T;
|
||||||
|
};
|
||||||
|
cta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
showCta?: T;
|
||||||
|
ctaText?: T;
|
||||||
|
ctaLink?: T;
|
||||||
|
ctaStyle?: T;
|
||||||
|
};
|
||||||
|
backgroundColor?: T;
|
||||||
|
borderRadius?: T;
|
||||||
|
shadow?: T;
|
||||||
|
spacing?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
seo?:
|
seo?:
|
||||||
| T
|
| T
|
||||||
|
|
@ -8490,6 +9091,270 @@ export interface EventsSelect<T extends boolean = true> {
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "bookings_select".
|
||||||
|
*/
|
||||||
|
export interface BookingsSelect<T extends boolean = true> {
|
||||||
|
tenant?: T;
|
||||||
|
customerName?: T;
|
||||||
|
customerEmail?: T;
|
||||||
|
customerPhone?: T;
|
||||||
|
customerCompany?: T;
|
||||||
|
serviceType?: T;
|
||||||
|
service?: T;
|
||||||
|
date?: T;
|
||||||
|
time?: T;
|
||||||
|
duration?: T;
|
||||||
|
locationType?: T;
|
||||||
|
locationAddress?: T;
|
||||||
|
participants?: T;
|
||||||
|
message?: T;
|
||||||
|
referenceImages?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
image?: T;
|
||||||
|
note?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
status?: T;
|
||||||
|
priority?: T;
|
||||||
|
pricing?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
estimatedPrice?: T;
|
||||||
|
finalPrice?: T;
|
||||||
|
depositAmount?: T;
|
||||||
|
depositPaid?: T;
|
||||||
|
fullyPaid?: T;
|
||||||
|
};
|
||||||
|
internalNotes?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
note?: T;
|
||||||
|
author?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
contactHistory?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
type?: T;
|
||||||
|
summary?: T;
|
||||||
|
date?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
assignedTo?: T;
|
||||||
|
source?: T;
|
||||||
|
gdprConsent?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "certifications_select".
|
||||||
|
*/
|
||||||
|
export interface CertificationsSelect<T extends boolean = true> {
|
||||||
|
tenant?: T;
|
||||||
|
name?: T;
|
||||||
|
slug?: T;
|
||||||
|
description?: T;
|
||||||
|
shortDescription?: T;
|
||||||
|
type?: T;
|
||||||
|
category?: T;
|
||||||
|
issuer?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
logo?: T;
|
||||||
|
website?: T;
|
||||||
|
country?: T;
|
||||||
|
};
|
||||||
|
certNumber?: T;
|
||||||
|
issuedDate?: T;
|
||||||
|
validUntil?: T;
|
||||||
|
renewalCycle?: T;
|
||||||
|
logo?: T;
|
||||||
|
certificate?: T;
|
||||||
|
gallery?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
document?: T;
|
||||||
|
title?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
scope?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
description?: T;
|
||||||
|
locations?: T;
|
||||||
|
services?: T;
|
||||||
|
};
|
||||||
|
requirements?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
requirement?: T;
|
||||||
|
description?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
benefits?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
description?: T;
|
||||||
|
icon?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
audits?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
date?: T;
|
||||||
|
type?: T;
|
||||||
|
result?: T;
|
||||||
|
notes?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
status?: T;
|
||||||
|
visibility?: T;
|
||||||
|
priority?: T;
|
||||||
|
showOnHomepage?: T;
|
||||||
|
seo?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
metaTitle?: T;
|
||||||
|
metaDescription?: T;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "projects_select".
|
||||||
|
*/
|
||||||
|
export interface ProjectsSelect<T extends boolean = true> {
|
||||||
|
tenant?: T;
|
||||||
|
title?: T;
|
||||||
|
slug?: T;
|
||||||
|
tagline?: T;
|
||||||
|
description?: T;
|
||||||
|
shortDescription?: T;
|
||||||
|
type?: T;
|
||||||
|
genres?: T;
|
||||||
|
platforms?: T;
|
||||||
|
featuredImage?: T;
|
||||||
|
logo?: T;
|
||||||
|
screenshots?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
image?: T;
|
||||||
|
caption?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
videos?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
type?: T;
|
||||||
|
title?: T;
|
||||||
|
url?: T;
|
||||||
|
thumbnail?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
techStack?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
engine?: T;
|
||||||
|
languages?: T;
|
||||||
|
tools?: T;
|
||||||
|
};
|
||||||
|
requirements?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
minimum?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
os?: T;
|
||||||
|
cpu?: T;
|
||||||
|
ram?: T;
|
||||||
|
gpu?: T;
|
||||||
|
storage?: T;
|
||||||
|
};
|
||||||
|
recommended?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
os?: T;
|
||||||
|
cpu?: T;
|
||||||
|
ram?: T;
|
||||||
|
gpu?: T;
|
||||||
|
storage?: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
releaseDate?: T;
|
||||||
|
links?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
website?: T;
|
||||||
|
steam?: T;
|
||||||
|
itchio?: T;
|
||||||
|
epicGames?: T;
|
||||||
|
gog?: T;
|
||||||
|
playStore?: T;
|
||||||
|
appStore?: T;
|
||||||
|
github?: T;
|
||||||
|
discord?: T;
|
||||||
|
twitter?: T;
|
||||||
|
};
|
||||||
|
downloads?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
platform?: T;
|
||||||
|
version?: T;
|
||||||
|
file?: T;
|
||||||
|
externalUrl?: T;
|
||||||
|
size?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
features?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
description?: T;
|
||||||
|
icon?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
team?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
role?: T;
|
||||||
|
link?: T;
|
||||||
|
avatar?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
gameJam?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
jamName?: T;
|
||||||
|
theme?: T;
|
||||||
|
duration?: T;
|
||||||
|
ranking?: T;
|
||||||
|
jamLink?: T;
|
||||||
|
};
|
||||||
|
devlogs?: T;
|
||||||
|
status?: T;
|
||||||
|
visibility?: T;
|
||||||
|
featured?: T;
|
||||||
|
sortOrder?: T;
|
||||||
|
seo?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
metaTitle?: T;
|
||||||
|
metaDescription?: T;
|
||||||
|
ogImage?: T;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "cookie-configurations_select".
|
* via the `definition` "cookie-configurations_select".
|
||||||
|
|
|
||||||
|
|
@ -209,8 +209,8 @@ export default buildConfig({
|
||||||
pool: {
|
pool: {
|
||||||
connectionString: env.DATABASE_URI,
|
connectionString: env.DATABASE_URI,
|
||||||
},
|
},
|
||||||
// Temporär aktiviert für Events Collection
|
// push: false - Schema-Änderungen nur via Migrationen
|
||||||
push: true,
|
push: false,
|
||||||
}),
|
}),
|
||||||
// Sharp für Bildoptimierung
|
// Sharp für Bildoptimierung
|
||||||
sharp,
|
sharp,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue