Compare commits
13 Commits
v2.3.20
...
v2.3.22-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| 263640bce5 | |||
| 1cc8fa4db4 | |||
| 1db7f3af6c | |||
| 909bad573b | |||
| 24a19adf19 | |||
| 3313206734 | |||
| ec989690ce | |||
| 37807079cd | |||
| af4213ad59 | |||
| dc1ba4def3 | |||
| f87c714402 | |||
| f989e0604f | |||
| 4a2d094cbd |
@@ -532,6 +532,7 @@ jobs:
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET }}
|
||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||
run: pnpm run check:forms
|
||||
|
||||
|
||||
25
app/api/health/fix-avb-now/route.ts
Normal file
25
app/api/health/fix-avb-now/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getPayload } from 'payload';
|
||||
import config from '@/payload.config';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
// Clear the excerpt for page ID 6
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: 6,
|
||||
data: {
|
||||
excerpt: '',
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: 'AVB excerpt cleared successfully on production!' });
|
||||
} catch (error: any) {
|
||||
console.error('Error fixing AVB:', error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,38 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ slug
|
||||
try {
|
||||
const { slug } = await params;
|
||||
|
||||
// Hardcoded bypass for AGBs to use the original uploaded PDF.
|
||||
// Handle AGBs specifically - either fetch from collection or use fallback file
|
||||
if (slug === 'agbs') {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const currentAgbs = await payload.find({
|
||||
collection: 'agbs-collection',
|
||||
where: {
|
||||
isCurrent: { equals: true },
|
||||
},
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (currentAgbs.totalDocs > 0) {
|
||||
const agb = currentAgbs.docs[0];
|
||||
const media = agb.file as any;
|
||||
if (media && media.url) {
|
||||
// If it's a remote URL or absolute path, we might need to fetch it
|
||||
// For now assume it's a local file in public/media
|
||||
const filePath = path.join(process.cwd(), 'public', media.url);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
return new NextResponse(fileBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="${media.filename}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for hardcoded legacy AGB
|
||||
const filePath = path.join(process.cwd(), 'public', 'AVB-KLZ-4-2026.pdf');
|
||||
if (fs.existsSync(filePath)) {
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
|
||||
16
app/health/fix-avb/route.ts
Normal file
16
app/health/fix-avb/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function GET() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: 6,
|
||||
data: {
|
||||
excerpt: '',
|
||||
},
|
||||
})
|
||||
|
||||
return Response.json({ success: true, message: 'AVB excerpt cleared' })
|
||||
}
|
||||
76
components/AgbHistoryBlock.tsx
Normal file
76
components/AgbHistoryBlock.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
import { Heading } from '@/components/ui';
|
||||
|
||||
export const AgbHistoryBlock: React.FC<{ title: string }> = async ({ title }) => {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
const agbs = await payload.find({
|
||||
collection: 'agbs-collection',
|
||||
sort: '-versionDate',
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
if (agbs.totalDocs === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-16 p-8 md:p-12 bg-neutral-light rounded-3xl shadow-sm border border-neutral-medium">
|
||||
<Heading level={3} className="mb-8 text-saturated">
|
||||
{title || 'Vorherige Versionen'}
|
||||
</Heading>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{agbs.docs.map((agb: any) => {
|
||||
const date = new Date(agb.versionDate).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const fileUrl = typeof agb.file === 'object' ? agb.file.url : '';
|
||||
const filename = typeof agb.file === 'object' ? agb.file.filename : 'agb.pdf';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={agb.id}
|
||||
className="flex items-center justify-between p-6 bg-white rounded-2xl shadow-sm border border-neutral-medium hover:border-primary transition-all group"
|
||||
>
|
||||
<div>
|
||||
<h4 className="font-bold text-saturated group-hover:text-primary transition-colors">
|
||||
{agb.title}
|
||||
</h4>
|
||||
<p className="text-sm text-text-secondary mt-1">Gültig ab {date}</p>
|
||||
</div>
|
||||
{fileUrl && (
|
||||
<a
|
||||
href={fileUrl}
|
||||
download={filename}
|
||||
className="p-3 bg-neutral-light text-primary rounded-full hover:bg-primary hover:text-white transition-all shadow-sm"
|
||||
title="PDF herunterladen"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" x2="12" y1="15" y2="3" />
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -38,6 +38,7 @@ import GallerySection from '@/components/home/GallerySection';
|
||||
import VideoSection from '@/components/home/VideoSection';
|
||||
import CTA from '@/components/home/CTA';
|
||||
import { PDFDownloadBlock } from '@/components/PDFDownloadBlock';
|
||||
import { AgbHistoryBlock } from '@/components/AgbHistoryBlock';
|
||||
|
||||
/**
|
||||
* Splits a text string on \n and intersperses <br /> elements.
|
||||
@@ -436,6 +437,8 @@ const jsxConverters: JSXConverters = {
|
||||
'block-pdfDownload': ({ node }: any) => (
|
||||
<PDFDownloadBlock label={node.fields.label} style={node.fields.style} />
|
||||
),
|
||||
agbHistory: ({ node }: any) => <AgbHistoryBlock title={node.fields.title} />,
|
||||
'block-agbHistory': ({ node }: any) => <AgbHistoryBlock title={node.fields.title} />,
|
||||
// ─── New Page Blocks ───────────────────────────────────────────
|
||||
heroSection: ({ node }: any) => {
|
||||
const f = node.fields;
|
||||
@@ -793,8 +796,8 @@ const jsxConverters: JSXConverters = {
|
||||
</Section>
|
||||
);
|
||||
},
|
||||
imageGallery: ({ node }: any) => <Gallery />,
|
||||
'block-imageGallery': ({ node }: any) => <Gallery />,
|
||||
imageGallery: ({ node: _node }: any) => <Gallery />,
|
||||
'block-imageGallery': ({ node: _node }: any) => <Gallery />,
|
||||
categoryGrid: ({ node }: any) => {
|
||||
const cats = node.fields.categories || [];
|
||||
return (
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"legalNoticeSlug": "impressum",
|
||||
"privacyPolicy": "Datenschutz",
|
||||
"privacyPolicySlug": "datenschutz",
|
||||
"terms": "AGB",
|
||||
"terms": "AVB",
|
||||
"termsSlug": "terms",
|
||||
"products": "Produkte",
|
||||
"lowVoltage": "Niederspannungskabel",
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.3.20",
|
||||
"version": "2.3.22-rc.2",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface Config {
|
||||
'form-submissions': FormSubmission;
|
||||
products: Product;
|
||||
pages: Page;
|
||||
'agbs-collection': AgbsCollection;
|
||||
'payload-kv': PayloadKv;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
@@ -86,8 +87,11 @@ export interface Config {
|
||||
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
|
||||
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
'agbs-collection': AgbsCollectionSelect<false> | AgbsCollectionSelect<true>;
|
||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-locked-documents':
|
||||
| PayloadLockedDocumentsSelect<false>
|
||||
| PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
@@ -358,6 +362,19 @@ export interface Page {
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "agbs-collection".
|
||||
*/
|
||||
export interface AgbsCollection {
|
||||
id: number;
|
||||
title: string;
|
||||
versionDate: string;
|
||||
file: number | Media;
|
||||
isCurrent?: boolean | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv".
|
||||
@@ -405,6 +422,10 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: number | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'agbs-collection';
|
||||
value: number | AgbsCollection;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -592,6 +613,18 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "agbs-collection_select".
|
||||
*/
|
||||
export interface AgbsCollectionSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
versionDate?: T;
|
||||
file?: T;
|
||||
isCurrent?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
@@ -980,7 +1013,6 @@ export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Posts } from './src/payload/collections/Posts';
|
||||
import { FormSubmissions } from './src/payload/collections/FormSubmissions';
|
||||
import { Products } from './src/payload/collections/Products';
|
||||
import { Pages } from './src/payload/collections/Pages';
|
||||
import { Agbs } from './src/payload/collections/Agbs';
|
||||
import { seedDatabase } from './src/payload/seed';
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
@@ -56,7 +57,7 @@ export default buildConfig({
|
||||
defaultLocale: 'de',
|
||||
fallback: true,
|
||||
},
|
||||
collections: [Users, Media, Posts, FormSubmissions, Products, Pages],
|
||||
collections: [Users, Media, Posts, FormSubmissions, Products, Pages, Agbs],
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
|
||||
@@ -348,13 +348,23 @@ async function main() {
|
||||
|
||||
// 5. Cleanup: Delete test submissions from Payload CMS
|
||||
console.log(`\n🧹 Starting cleanup of test submissions...`);
|
||||
const payloadSecret = process.env.PAYLOAD_SECRET;
|
||||
|
||||
if (!payloadSecret) {
|
||||
console.warn(` ⚠️ PAYLOAD_SECRET not found in environment. Cleanup will likely fail with 403.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const apiUrl = `${targetUrl.replace(/\/$/, '')}/api/form-submissions`;
|
||||
const searchUrl = `${apiUrl}?where[email][equals]=testing@mintel.me`;
|
||||
// We fetch ALL submissions matching the test email to clean up potential lefovers from previous runs
|
||||
const searchUrl = `${apiUrl}?where[email][equals]=testing@mintel.me&limit=100`;
|
||||
|
||||
// Fetch test submissions
|
||||
// Fetch test submissions with bypass header
|
||||
const searchResponse = await axios.get(searchUrl, {
|
||||
headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: {
|
||||
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
|
||||
'x-e2e-secret': payloadSecret || '',
|
||||
},
|
||||
});
|
||||
|
||||
const testSubmissions = searchResponse.data.docs || [];
|
||||
@@ -363,25 +373,30 @@ async function main() {
|
||||
for (const doc of testSubmissions) {
|
||||
try {
|
||||
await axios.delete(`${apiUrl}/${doc.id}`, {
|
||||
headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: {
|
||||
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
|
||||
'x-e2e-secret': payloadSecret || '',
|
||||
},
|
||||
});
|
||||
console.log(` ✅ Deleted submission: ${doc.id}`);
|
||||
console.log(` ✅ Deleted submission: ${doc.id} (${doc.name})`);
|
||||
} catch (delErr: any) {
|
||||
// Log but don't fail, 403s on Directus / Payload APIs for guest Gatekeeper sessions are normal
|
||||
console.warn(
|
||||
` ⚠️ Cleanup attempt on ${doc.id} returned an error, typically due to API Auth separation: ${delErr.message}`,
|
||||
` ⚠️ Cleanup attempt on ${doc.id} failed: ${delErr.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (testSubmissions.length > 0) {
|
||||
console.log(`✅ Cleanup completed successfully.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.response?.status === 403) {
|
||||
console.warn(
|
||||
` ⚠️ Cleanup fetch failed with 403 Forbidden. This is expected if the runner lacks admin API credentials. Test submissions remain in the database.`,
|
||||
console.error(
|
||||
` ❌ Cleanup failed with 403 Forbidden. Ensure FormSubmissions access control and PAYLOAD_SECRET are aligned.`,
|
||||
);
|
||||
} else {
|
||||
console.error(` ❌ Cleanup fetch failed: ${err.message}`);
|
||||
}
|
||||
// Don't mark the whole test as failed just because cleanup failed
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
@@ -22,6 +22,7 @@ async function run() {
|
||||
id: 6,
|
||||
data: {
|
||||
title: 'Allgemeine Verkaufsbedingungen (AVB)',
|
||||
excerpt: '',
|
||||
content: lexicalContent,
|
||||
},
|
||||
});
|
||||
|
||||
18
src/payload/blocks/AgbHistory.ts
Normal file
18
src/payload/blocks/AgbHistory.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const AgbHistory: Block = {
|
||||
slug: 'agbHistory',
|
||||
labels: {
|
||||
singular: 'AGB Historie',
|
||||
plural: 'AGB Historien',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
label: 'Titel',
|
||||
localized: true,
|
||||
defaultValue: 'Vorherige Versionen',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import { TeamProfile } from './TeamProfile';
|
||||
import { TechnicalGrid } from './TechnicalGrid';
|
||||
import { VisualLinkPreview } from './VisualLinkPreview';
|
||||
import { PDFDownload } from './PDFDownload';
|
||||
import { AgbHistory } from './AgbHistory';
|
||||
import { homeBlocksArray } from './HomeBlocks';
|
||||
|
||||
export const payloadBlocks = [
|
||||
@@ -40,4 +41,5 @@ export const payloadBlocks = [
|
||||
TechnicalGrid,
|
||||
VisualLinkPreview,
|
||||
PDFDownload,
|
||||
AgbHistory,
|
||||
];
|
||||
|
||||
49
src/payload/collections/Agbs.ts
Normal file
49
src/payload/collections/Agbs.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { CollectionConfig } from 'payload';
|
||||
|
||||
export const Agbs: CollectionConfig = {
|
||||
slug: 'agbs-collection',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'versionDate', 'updatedAt'],
|
||||
group: 'Rechtliches',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
label: 'Titel (z.B. AGB April 2026)',
|
||||
},
|
||||
{
|
||||
name: 'versionDate',
|
||||
type: 'date',
|
||||
required: true,
|
||||
label: 'Gültig ab',
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayOnly',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'file',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: true,
|
||||
label: 'PDF Datei',
|
||||
},
|
||||
{
|
||||
name: 'isCurrent',
|
||||
type: 'checkbox',
|
||||
label: 'Aktuelle Version',
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -9,9 +9,19 @@ export const FormSubmissions: CollectionConfig = {
|
||||
},
|
||||
access: {
|
||||
// Only Admins can view and delete leads via dashboard.
|
||||
read: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
|
||||
update: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
|
||||
delete: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
|
||||
// E2E scripts can bypass if they provide the correct secret header.
|
||||
read: ({ req }) => {
|
||||
const isE2E = req.headers.get('x-e2e-secret') === process.env.PAYLOAD_SECRET;
|
||||
return Boolean(req.user) || isE2E || process.env.NODE_ENV === 'development';
|
||||
},
|
||||
update: ({ req }) => {
|
||||
const isE2E = req.headers.get('x-e2e-secret') === process.env.PAYLOAD_SECRET;
|
||||
return Boolean(req.user) || isE2E || process.env.NODE_ENV === 'development';
|
||||
},
|
||||
delete: ({ req }) => {
|
||||
const isE2E = req.headers.get('x-e2e-secret') === process.env.PAYLOAD_SECRET;
|
||||
return Boolean(req.user) || isE2E || process.env.NODE_ENV === 'development';
|
||||
},
|
||||
// Next.js server actions handle secure inserts natively. No public client create access.
|
||||
create: () => false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user