cms.c2sgmbh/src/collections/Tenants.ts
Martin Porwoll da735cab46 feat: add Products and ProductCategories collections with CI/CD pipeline
- Add Products collection with comprehensive fields (pricing, inventory, SEO, CTA)
- Add ProductCategories collection with hierarchical structure
- Implement CI/CD pipeline with GitHub Actions (lint, typecheck, test, build, e2e)
- Add access control test utilities and unit tests
- Fix Posts API to include category field for backwards compatibility
- Update ESLint config with ignores for migrations and admin components
- Add centralized access control functions in src/lib/access
- Add db-direct.sh utility script for database access

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 21:36:26 +00:00

254 lines
7.7 KiB
TypeScript

import type { CollectionConfig, FieldHook } from 'payload'
import { invalidateEmailCacheHook } from '../hooks/invalidateEmailCache'
import { auditTenantAfterChange, auditTenantAfterDelete } from '../hooks/auditTenantChanges'
import { neverReadable } from '../lib/access'
/**
* Validiert SMTP Host Format
*/
const validateSmtpHost: FieldHook = ({ value, siblingData }) => {
// Nur validieren wenn useCustomSmtp aktiv ist und ein Wert vorhanden
const emailData = siblingData as { useCustomSmtp?: boolean }
if (!emailData?.useCustomSmtp) return value
if (!value) return value
// Basis-Validierung: Keine Protokoll-Präfixe erlaubt
if (value.includes('://')) {
throw new Error('SMTP Host ohne Protokoll angeben (z.B. smtp.example.com)')
}
return value
}
/**
* Validiert SMTP Port
*/
const validateSmtpPort: FieldHook = ({ value, siblingData }) => {
const emailData = siblingData as { useCustomSmtp?: boolean }
if (!emailData?.useCustomSmtp) return value
const port = Number(value)
if (port && (port < 1 || port > 65535)) {
throw new Error('Port muss zwischen 1 und 65535 liegen')
}
return value
}
export const Tenants: CollectionConfig = {
slug: 'tenants',
admin: {
useAsTitle: 'name',
},
hooks: {
afterChange: [invalidateEmailCacheHook, auditTenantAfterChange],
afterDelete: [auditTenantAfterDelete],
},
fields: [
{
name: 'name',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
},
{
name: 'domains',
type: 'array',
fields: [
{
name: 'domain',
type: 'text',
required: true,
},
],
},
// E-Mail Konfiguration
{
name: 'email',
type: 'group',
label: 'E-Mail Konfiguration',
admin: {
description: 'SMTP-Einstellungen für diesen Tenant. Leer = globale Einstellungen.',
},
fields: [
{
type: 'row',
fields: [
{
name: 'fromAddress',
type: 'email',
label: 'Absender E-Mail',
admin: {
placeholder: 'info@domain.de',
width: '50%',
description:
'Tipp: Verwenden Sie eine E-Mail-Adresse der Domain, für die SPF/DKIM konfiguriert ist.',
},
},
{
name: 'fromName',
type: 'text',
label: 'Absender Name',
admin: {
placeholder: 'Firmenname',
width: '50%',
},
},
],
},
{
name: 'replyTo',
type: 'email',
label: 'Antwort-Adresse (Reply-To)',
admin: {
placeholder: 'kontakt@domain.de (optional)',
},
},
// SPF/DKIM Info-Block
{
name: 'emailDeliverabilityInfo',
type: 'ui',
admin: {
components: {
Field: '@/components/admin/EmailDeliverabilityInfo#EmailDeliverabilityInfo',
},
},
},
{
name: 'useCustomSmtp',
type: 'checkbox',
label: 'Eigenen SMTP-Server verwenden',
defaultValue: false,
admin: {
description:
'Aktivieren Sie diese Option, um einen eigenen SMTP-Server statt der globalen Einstellungen zu verwenden.',
},
},
{
name: 'smtp',
type: 'group',
label: 'SMTP Einstellungen',
admin: {
condition: (_, siblingData) => siblingData?.useCustomSmtp,
description:
'Hinweis: Stellen Sie sicher, dass SPF- und DKIM-Einträge für Ihre Domain konfiguriert sind, um eine optimale E-Mail-Zustellung zu gewährleisten.',
},
fields: [
{
type: 'row',
fields: [
{
name: 'host',
type: 'text',
label: 'SMTP Host',
required: true,
admin: {
placeholder: 'smtp.example.com',
width: '50%',
description: 'Hostname ohne Protokoll (z.B. smtp.gmail.com)',
},
hooks: {
beforeValidate: [validateSmtpHost],
},
validate: (value: string | undefined | null, { siblingData }: { siblingData: Record<string, unknown> }) => {
const emailData = siblingData as { useCustomSmtp?: boolean }
if (emailData?.useCustomSmtp && !value) {
return 'SMTP Host ist erforderlich'
}
return true
},
},
{
name: 'port',
type: 'number',
label: 'Port',
defaultValue: 587,
admin: {
width: '25%',
description: '587 (STARTTLS) oder 465 (SSL)',
},
hooks: {
beforeValidate: [validateSmtpPort],
},
min: 1,
max: 65535,
},
{
name: 'secure',
type: 'checkbox',
label: 'SSL/TLS (Port 465)',
defaultValue: false,
admin: {
width: '25%',
description: 'Für Port 465 aktivieren',
},
},
],
},
{
type: 'row',
fields: [
{
name: 'user',
type: 'text',
label: 'SMTP Benutzername',
required: true,
admin: {
width: '50%',
description: 'Meist die E-Mail-Adresse',
},
validate: (value: string | undefined | null, { siblingData }: { siblingData: Record<string, unknown> }) => {
const smtpData = siblingData as { host?: string }
// Nur validieren wenn host gesetzt ist (d.h. SMTP aktiv)
if (smtpData?.host && !value) {
return 'SMTP Benutzername ist erforderlich'
}
return true
},
},
{
name: 'pass',
type: 'text',
label: 'SMTP Passwort',
admin: {
width: '50%',
description: 'Leer lassen um bestehendes Passwort zu behalten',
},
access: {
read: neverReadable, // Passwort nie in API-Response
},
hooks: {
beforeChange: [
({ value, originalDoc }) => {
// Behalte altes Passwort wenn Feld leer
if (!value && originalDoc?.email?.smtp?.pass) {
return originalDoc.email.smtp.pass
}
return value
},
],
},
},
],
},
// Test-Email Button
{
name: 'testEmailButton',
type: 'ui',
admin: {
components: {
Field: '@/components/admin/TestEmailButton#TestEmailButton',
},
},
},
],
},
],
},
],
}