fix: resolve all TypeScript errors in production code

- Add Where type imports and proper type assertions in API routes
- Add Locale type definitions for locale validation
- Fix email-logs/stats route with proper EmailLog typing
- Fix newsletter-service interests type and null checks
- Remove invalid contact field from OpenAPI metadata
- Fix formSubmissionOverrides type casting in payload.config
- Fix vcard route Team type casting

All 24 TypeScript errors in src/ are now resolved.
Test files have separate type issues that don't affect production.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2025-12-15 09:08:16 +00:00
parent 2faefdac1e
commit 9016d3c06c
9 changed files with 54 additions and 38 deletions

View file

@ -3,8 +3,11 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import type { Category, Media, Post } from '@/payload-types' import type { Category, Media, Post } from '@/payload-types'
type Locale = 'de' | 'en' | 'all'
import { import {
searchLimiter, searchLimiter,
rateLimitHeaders, rateLimitHeaders,
@ -49,8 +52,8 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
const includeRelated = searchParams.get('includeRelated') !== 'false' // Default true const includeRelated = searchParams.get('includeRelated') !== 'false' // Default true
// Validate locale // Validate locale
const validLocales = ['de', 'en'] const validLocales: Locale[] = ['de', 'en']
const locale = localeParam && validLocales.includes(localeParam) ? localeParam : 'de' const locale: Locale = localeParam && validLocales.includes(localeParam as Locale) ? (localeParam as Locale) : 'de'
// Parse tenant ID - REQUIRED for tenant isolation // Parse tenant ID - REQUIRED for tenant isolation
const tenantId = tenantParam ? parseInt(tenantParam, 10) : undefined const tenantId = tenantParam ? parseInt(tenantParam, 10) : undefined
@ -68,7 +71,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
const payload = await getPayload({ config }) const payload = await getPayload({ config })
// Build where clause (tenant is now required) // Build where clause (tenant is now required)
const where: Record<string, unknown> = { const where: Where = {
slug: { equals: slug }, slug: { equals: slug },
status: { equals: 'published' }, status: { equals: 'published' },
tenant: { equals: tenantId }, tenant: { equals: tenantId },

View file

@ -3,8 +3,11 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import type { Category, Media, Post } from '@/payload-types' import type { Category, Media, Post } from '@/payload-types'
type Locale = 'de' | 'en' | 'all'
import { import {
searchLimiter, searchLimiter,
rateLimitHeaders, rateLimitHeaders,
@ -29,7 +32,7 @@ interface NewsQueryParams {
search?: string search?: string
year?: number year?: number
month?: number month?: number
locale: string locale: Locale
page: number page: number
limit: number limit: number
excludeIds?: number[] excludeIds?: number[]
@ -51,7 +54,7 @@ async function getNews(payload: Awaited<ReturnType<typeof getPayload>>, params:
} = params } = params
// Build where clause // Build where clause
const where: Record<string, unknown> = { const where: Where = {
status: { equals: 'published' }, status: { equals: 'published' },
} }
@ -131,7 +134,7 @@ async function getNews(payload: Awaited<ReturnType<typeof getPayload>>, params:
// Execute query // Execute query
return payload.find({ return payload.find({
collection: 'posts', collection: 'posts',
where, where: where as Where,
sort: '-publishedAt', sort: '-publishedAt',
page, page,
limit, limit,
@ -144,16 +147,16 @@ async function getNews(payload: Awaited<ReturnType<typeof getPayload>>, params:
async function getCategories( async function getCategories(
payload: Awaited<ReturnType<typeof getPayload>>, payload: Awaited<ReturnType<typeof getPayload>>,
tenantId?: number, tenantId?: number,
locale: string = 'de' locale: Locale = 'de'
) { ) {
const where: Record<string, unknown> = {} const where: Where = {}
if (tenantId) { if (tenantId) {
where.tenant = { equals: tenantId } where.tenant = { equals: tenantId }
} }
const result = await payload.find({ const result = await payload.find({
collection: 'categories', collection: 'categories',
where, where: where as Where,
sort: 'name', sort: 'name',
limit: 100, limit: 100,
locale, locale,
@ -172,7 +175,7 @@ async function getArchive(
payload: Awaited<ReturnType<typeof getPayload>>, payload: Awaited<ReturnType<typeof getPayload>>,
tenantId: number // Now required tenantId: number // Now required
) { ) {
const where: Record<string, unknown> = { const where: Where = {
status: { equals: 'published' }, status: { equals: 'published' },
publishedAt: { exists: true }, publishedAt: { exists: true },
tenant: { equals: tenantId }, tenant: { equals: tenantId },
@ -187,7 +190,7 @@ async function getArchive(
while (hasMore) { while (hasMore) {
const result = await payload.find({ const result = await payload.find({
collection: 'posts', collection: 'posts',
where, where: where as Where,
sort: '-publishedAt', sort: '-publishedAt',
page, page,
limit: pageSize, limit: pageSize,
@ -270,8 +273,8 @@ export async function GET(request: NextRequest) {
const includeArchive = searchParams.get('includeArchive') === 'true' const includeArchive = searchParams.get('includeArchive') === 'true'
// Validate locale // Validate locale
const validLocales = ['de', 'en'] const validLocales: Locale[] = ['de', 'en']
const locale = localeParam && validLocales.includes(localeParam) ? localeParam : 'de' const locale: Locale = localeParam && validLocales.includes(localeParam as Locale) ? (localeParam as Locale) : 'de'
// Validate and parse types // Validate and parse types
let types: NewsType | NewsType[] | undefined let types: NewsType | NewsType[] | undefined

View file

@ -50,7 +50,7 @@ export async function GET(
} }
// Generate vCard 3.0 // Generate vCard 3.0
const vcard = generateVCard(member) const vcard = generateVCard(member as TeamMember)
// Return as downloadable file // Return as downloadable file
const filename = `${member.slug || member.name?.toLowerCase().replace(/\s+/g, '-')}.vcf` const filename = `${member.slug || member.name?.toLowerCase().replace(/\s+/g, '-')}.vcf`

View file

@ -1,7 +1,10 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config' import config from '@payload-config'
type Locale = 'de' | 'en' | 'all'
/** /**
* Team API * Team API
* *
@ -44,10 +47,11 @@ export async function GET(request: NextRequest) {
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 100) const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 100)
const page = parseInt(searchParams.get('page') || '1') const page = parseInt(searchParams.get('page') || '1')
const sort = searchParams.get('sort') || 'order' const sort = searchParams.get('sort') || 'order'
const locale = (searchParams.get('locale') as 'de' | 'en') || 'de' const localeParam = searchParams.get('locale')
const locale: Locale = (localeParam === 'de' || localeParam === 'en') ? localeParam : 'de'
// Build where clause // Build where clause
const where: Record<string, unknown> = { const where: Where = {
tenant: { equals: parseInt(tenantId) }, tenant: { equals: parseInt(tenantId) },
isActive: { equals: true }, isActive: { equals: true },
} }
@ -94,7 +98,7 @@ export async function GET(request: NextRequest) {
// Query team members // Query team members
const result = await payload.find({ const result = await payload.find({
collection: 'team', collection: 'team',
where, where: where as Where,
sort: sortField, sort: sortField,
limit, limit,
page, page,

View file

@ -3,8 +3,11 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import type { Media } from '@/payload-types' import type { Media } from '@/payload-types'
type Locale = 'de' | 'en' | 'all'
import { import {
searchLimiter, searchLimiter,
rateLimitHeaders, rateLimitHeaders,
@ -127,8 +130,8 @@ export async function GET(request: NextRequest) {
} }
// Validate locale // Validate locale
const validLocales = ['de', 'en'] const validLocales: Locale[] = ['de', 'en']
const locale = localeParam && validLocales.includes(localeParam) ? localeParam : 'de' const locale: Locale = localeParam && validLocales.includes(localeParam as Locale) ? (localeParam as Locale) : 'de'
// Validate type if provided // Validate type if provided
if (typeParam && !TIMELINE_TYPES.includes(typeParam as TimelineType)) { if (typeParam && !TIMELINE_TYPES.includes(typeParam as TimelineType)) {
@ -139,7 +142,7 @@ export async function GET(request: NextRequest) {
} }
// Build where clause // Build where clause
const where: Record<string, unknown> = { const where: Where = {
status: { equals: 'published' }, status: { equals: 'published' },
tenant: { equals: tenantId }, tenant: { equals: tenantId },
} }
@ -160,7 +163,7 @@ export async function GET(request: NextRequest) {
// Execute query // Execute query
const result = await payload.find({ const result = await payload.find({
collection: 'timelines', collection: 'timelines',
where, where: where as Where,
sort: '-updatedAt', sort: '-updatedAt',
limit: slugParam ? 1 : 100, // Single or list limit: slugParam ? 1 : 100, // Single or list
locale, locale,

View file

@ -3,8 +3,11 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import type { Media } from '@/payload-types' import type { Media } from '@/payload-types'
type Locale = 'de' | 'en' | 'all'
import { import {
searchLimiter, searchLimiter,
rateLimitHeaders, rateLimitHeaders,
@ -122,8 +125,8 @@ export async function GET(request: NextRequest) {
} }
// Validate locale // Validate locale
const validLocales = ['de', 'en'] const validLocales: Locale[] = ['de', 'en']
const locale = localeParam && validLocales.includes(localeParam) ? localeParam : 'de' const locale: Locale = localeParam && validLocales.includes(localeParam as Locale) ? (localeParam as Locale) : 'de'
// Validate type if provided // Validate type if provided
if (typeParam && !WORKFLOW_TYPES.includes(typeParam as WorkflowType)) { if (typeParam && !WORKFLOW_TYPES.includes(typeParam as WorkflowType)) {
@ -142,7 +145,7 @@ export async function GET(request: NextRequest) {
} }
// Build where clause // Build where clause
const where: Record<string, unknown> = { const where: Where = {
status: { equals: 'published' }, status: { equals: 'published' },
tenant: { equals: tenantId }, tenant: { equals: tenantId },
} }
@ -163,7 +166,7 @@ export async function GET(request: NextRequest) {
// Execute query // Execute query
const result = await payload.find({ const result = await payload.find({
collection: 'workflows', collection: 'workflows',
where, where: where as Where,
sort: '-updatedAt', sort: '-updatedAt',
limit: slugParam ? 1 : 100, // Single or list limit: slugParam ? 1 : 100, // Single or list
locale, locale,

View file

@ -12,8 +12,10 @@
*/ */
import { getPayload } from 'payload' import { getPayload } from 'payload'
import type { Where } from 'payload'
import configPromise from '@payload-config' import configPromise from '@payload-config'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import type { EmailLog } from '@/payload-types'
import { logAccessDenied } from '@/lib/audit/audit-service' import { logAccessDenied } from '@/lib/audit/audit-service'
import { maskSmtpError } from '@/lib/security/data-masking' import { maskSmtpError } from '@/lib/security/data-masking'
@ -89,7 +91,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
const periodDate = getPeriodDate(period) const periodDate = getPeriodDate(period)
// Basis-Where für alle Queries // Basis-Where für alle Queries
const baseWhere: Record<string, unknown> = { const baseWhere: Where = {
createdAt: { greater_than_equal: periodDate.toISOString() }, createdAt: { greater_than_equal: periodDate.toISOString() },
} }
@ -101,7 +103,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
// Gesamt // Gesamt
payload.count({ payload.count({
collection: 'email-logs', collection: 'email-logs',
where: baseWhere, where: baseWhere as Where,
}), }),
// Gesendet // Gesendet
payload.count({ payload.count({
@ -161,7 +163,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
successRate, successRate,
}, },
bySource: sourceStats, bySource: sourceStats,
recentFailures: recentFailed.docs.map((doc: Record<string, unknown>) => ({ recentFailures: recentFailed.docs.map((doc: EmailLog) => ({
id: doc.id, id: doc.id,
to: doc.to, to: doc.to,
subject: doc.subject, subject: doc.subject,

View file

@ -56,7 +56,7 @@ export class NewsletterService {
email: string email: string
firstName?: string firstName?: string
lastName?: string lastName?: string
interests?: string[] interests?: ('general' | 'blog' | 'products' | 'offers' | 'events')[]
source?: string source?: string
ipAddress?: string ipAddress?: string
userAgent?: string userAgent?: string
@ -245,12 +245,14 @@ export class NewsletterService {
}) })
// Tenant-ID ermitteln // Tenant-ID ermitteln
const tenantId = typeof subscriber.tenant === 'object' const tenantId = typeof subscriber.tenant === 'object' && subscriber.tenant
? subscriber.tenant.id ? subscriber.tenant.id
: subscriber.tenant : subscriber.tenant
// Willkommens-E-Mail senden // Willkommens-E-Mail senden
await this.sendWelcomeEmail(tenantId as number, subscriber) if (tenantId) {
await this.sendWelcomeEmail(tenantId as number, subscriber)
}
return { return {
success: true, success: true,
@ -308,7 +310,7 @@ export class NewsletterService {
} }
// Tenant-ID ermitteln // Tenant-ID ermitteln
const tenantId = typeof subscriber.tenant === 'object' const tenantId = typeof subscriber.tenant === 'object' && subscriber.tenant
? subscriber.tenant.id ? subscriber.tenant.id
: subscriber.tenant : subscriber.tenant

View file

@ -308,12 +308,12 @@ export default buildConfig({
// Fix für TypeScript Types Generation - das Plugin braucht explizite relationTo Angaben // Fix für TypeScript Types Generation - das Plugin braucht explizite relationTo Angaben
redirectRelationships: ['pages'], redirectRelationships: ['pages'],
formSubmissionOverrides: { formSubmissionOverrides: {
...formSubmissionOverrides, ...(formSubmissionOverrides as Record<string, unknown>),
hooks: { hooks: {
beforeChange: [formSubmissionBeforeChange], beforeChange: [formSubmissionBeforeChange],
afterChange: [sendFormNotification], afterChange: [sendFormNotification],
}, },
}, } as Parameters<typeof formBuilderPlugin>[0]['formSubmissionOverrides'],
}), }),
redirectsPlugin({ redirectsPlugin({
collections: ['pages'], collections: ['pages'],
@ -330,10 +330,6 @@ export default buildConfig({
title: 'Payload CMS API', title: 'Payload CMS API',
version: '1.0.0', version: '1.0.0',
description: 'Multi-Tenant CMS API für porwoll.de, complexcaresolutions.de, gunshin.de und zweitmein.ng', description: 'Multi-Tenant CMS API für porwoll.de, complexcaresolutions.de, gunshin.de und zweitmein.ng',
contact: {
name: 'C2S GmbH',
url: 'https://complexcaresolutions.de',
},
}, },
}), }),
// Swagger UI unter /api/docs // Swagger UI unter /api/docs