Compare commits
13 Commits
v2.3.18-rc
...
dc1ba4def3
| Author | SHA1 | Date | |
|---|---|---|---|
| dc1ba4def3 | |||
| f87c714402 | |||
| f989e0604f | |||
| 4a2d094cbd | |||
| 77181fc983 | |||
| 32b56696a6 | |||
| 9b28dd20d9 | |||
| 1970ae310f | |||
| 7bc8811a60 | |||
| 530503fa09 | |||
| a539e3c498 | |||
| 14574fb312 | |||
| fdeb34ea3c |
@@ -532,6 +532,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
|
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET }}
|
||||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||||
run: pnpm run check:forms
|
run: pnpm run check:forms
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,28 @@ import configPromise from '@payload-config';
|
|||||||
import { renderToStream } from '@react-pdf/renderer';
|
import { renderToStream } from '@react-pdf/renderer';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PDFPage } from '@/lib/pdf-page';
|
import { PDFPage } from '@/lib/pdf-page';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
export async function GET(req: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
|
export async function GET(req: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
|
||||||
try {
|
try {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|
||||||
|
// Hardcoded bypass for AGBs to use the original uploaded PDF.
|
||||||
|
if (slug === 'agbs') {
|
||||||
|
const filePath = path.join(process.cwd(), 'public', 'AVB-KLZ-4-2026.pdf');
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
const fileBuffer = fs.readFileSync(filePath);
|
||||||
|
return new NextResponse(fileBuffer, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': `attachment; filename="AVB-KLZ-4-2026.pdf"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get Payload App
|
// Get Payload App
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
/* eslint-disable react/prop-types */
|
|
||||||
import { Document, Page, View, Text, StyleSheet, Link, Image } from '@react-pdf/renderer';
|
import { Document, Page, View, Text, StyleSheet, Link, Image } from '@react-pdf/renderer';
|
||||||
|
|
||||||
// Standard fonts like Helvetica are built-in to PDF and don't require registration
|
// Standard fonts like Helvetica are built-in to PDF and don't require registration
|
||||||
@@ -27,7 +27,7 @@ const styles = StyleSheet.create({
|
|||||||
color: C.gray900,
|
color: C.gray900,
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.6,
|
||||||
backgroundColor: C.white,
|
backgroundColor: C.white,
|
||||||
paddingTop: 0,
|
paddingTop: 50,
|
||||||
paddingBottom: 80,
|
paddingBottom: 80,
|
||||||
fontFamily: 'Helvetica',
|
fontFamily: 'Helvetica',
|
||||||
},
|
},
|
||||||
@@ -35,10 +35,11 @@ const styles = StyleSheet.create({
|
|||||||
// Premium Header Layout
|
// Premium Header Layout
|
||||||
hero: {
|
hero: {
|
||||||
backgroundColor: C.offWhite,
|
backgroundColor: C.offWhite,
|
||||||
paddingTop: 40,
|
paddingTop: 24,
|
||||||
paddingBottom: 32,
|
paddingBottom: 24,
|
||||||
paddingHorizontal: MARGIN,
|
paddingHorizontal: MARGIN,
|
||||||
marginBottom: 40,
|
marginBottom: 40,
|
||||||
|
marginTop: -50, // Counters the page padding to achieve full-bleed top
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: C.gray200,
|
borderBottomColor: C.gray200,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -52,8 +53,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
logo: {
|
logo: {
|
||||||
width: 60,
|
width: 140, // Increased to fit full logo
|
||||||
height: 30,
|
height: 45,
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -71,12 +72,12 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
pageTitle: {
|
pageTitle: {
|
||||||
fontSize: 28,
|
fontSize: 16,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: C.navyDeep,
|
color: C.navyDeep,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: -0.5,
|
letterSpacing: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
accentBar: {
|
accentBar: {
|
||||||
@@ -243,7 +244,7 @@ const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
|
|||||||
case 'paragraph': {
|
case 'paragraph': {
|
||||||
if (!node.children || node.children.length === 0) return null;
|
if (!node.children || node.children.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<Text key={idx} style={styles.paragraph}>
|
<Text key={idx} style={styles.paragraph} minPresenceAhead={15}>
|
||||||
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -260,14 +261,14 @@ const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
|
|||||||
if (node.tag === 'h2') hStyle = styles.heading2;
|
if (node.tag === 'h2') hStyle = styles.heading2;
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<Text key={idx} style={hStyle}>
|
<Text key={idx} style={hStyle} wrap={false} minPresenceAhead={100}>
|
||||||
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isH1) {
|
if (isH1) {
|
||||||
return (
|
return (
|
||||||
<View key={idx} style={styles.heading1Wrapper}>
|
<View key={idx} style={styles.heading1Wrapper} wrap={false} minPresenceAhead={100}>
|
||||||
{content}
|
{content}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -283,7 +284,7 @@ const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
|
|||||||
{node.children?.map((child: any, i: number) => {
|
{node.children?.map((child: any, i: number) => {
|
||||||
if (child.type === 'listitem') {
|
if (child.type === 'listitem') {
|
||||||
return (
|
return (
|
||||||
<View key={i} style={styles.listItem}>
|
<View key={i} style={styles.listItem} wrap={false}>
|
||||||
<Text style={styles.listItemBullet}>
|
<Text style={styles.listItemBullet}>
|
||||||
{node.listType === 'number' ? `${i + 1}.` : '•'}
|
{node.listType === 'number' ? `${i + 1}.` : '•'}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -340,18 +341,15 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
|
|||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
});
|
});
|
||||||
|
|
||||||
const logoPath = `${process.cwd()}/public/logo.png`;
|
const logoPath = `${process.cwd()}/public/logo-full.png`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<Page size="A4" style={styles.page}>
|
||||||
{/* Improved Hero Header */}
|
{/* Improved Hero Header - No longer fixed so it doesn't repeat on all pages */}
|
||||||
<View style={styles.hero} fixed>
|
<View style={styles.hero}>
|
||||||
<View style={styles.headerTop}>
|
<View style={styles.headerTop}>
|
||||||
<Image src={logoPath} style={styles.logo} />
|
<Image src={logoPath} style={styles.logo} />
|
||||||
<Text style={styles.docType}>
|
|
||||||
{locale === 'en' ? 'Official Document' : 'Offizielles Dokument'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.headerTitleArea}>
|
<View style={styles.headerTitleArea}>
|
||||||
@@ -369,7 +367,7 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Industrial footer with page numbers */}
|
{/* Industrial footer with page numbers */}
|
||||||
<View style={{ ...styles.footer, position: 'absolute', bottom: 30, height: 40 }} fixed>
|
<View style={{ ...styles.footer, position: 'absolute', bottom: 40, height: 40 }} fixed>
|
||||||
<View style={styles.footerInfo}>
|
<View style={styles.footerInfo}>
|
||||||
<Text style={styles.footerBrand}>KLZ VERTRIEBS GMBH</Text>
|
<Text style={styles.footerBrand}>KLZ VERTRIEBS GMBH</Text>
|
||||||
<Text style={styles.footerText}>
|
<Text style={styles.footerText}>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
"legalNoticeSlug": "impressum",
|
"legalNoticeSlug": "impressum",
|
||||||
"privacyPolicy": "Datenschutz",
|
"privacyPolicy": "Datenschutz",
|
||||||
"privacyPolicySlug": "datenschutz",
|
"privacyPolicySlug": "datenschutz",
|
||||||
"terms": "AGB",
|
"terms": "AVB",
|
||||||
"termsSlug": "terms",
|
"termsSlug": "terms",
|
||||||
"products": "Produkte",
|
"products": "Produkte",
|
||||||
"lowVoltage": "Niederspannungskabel",
|
"lowVoltage": "Niederspannungskabel",
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.3.18-rc.6",
|
"version": "2.3.22-rc.1",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
BIN
public/AVB-KLZ-4-2026.docx
Normal file
BIN
public/AVB-KLZ-4-2026.docx
Normal file
Binary file not shown.
BIN
public/AVB-KLZ-4-2026.pdf
Normal file
BIN
public/AVB-KLZ-4-2026.pdf
Normal file
Binary file not shown.
BIN
public/logo-full.png
Normal file
BIN
public/logo-full.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
@@ -348,13 +348,23 @@ async function main() {
|
|||||||
|
|
||||||
// 5. Cleanup: Delete test submissions from Payload CMS
|
// 5. Cleanup: Delete test submissions from Payload CMS
|
||||||
console.log(`\n🧹 Starting cleanup of test submissions...`);
|
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 {
|
try {
|
||||||
const apiUrl = `${targetUrl.replace(/\/$/, '')}/api/form-submissions`;
|
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, {
|
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 || [];
|
const testSubmissions = searchResponse.data.docs || [];
|
||||||
@@ -363,25 +373,30 @@ async function main() {
|
|||||||
for (const doc of testSubmissions) {
|
for (const doc of testSubmissions) {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`${apiUrl}/${doc.id}`, {
|
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) {
|
} catch (delErr: any) {
|
||||||
// Log but don't fail, 403s on Directus / Payload APIs for guest Gatekeeper sessions are normal
|
|
||||||
console.warn(
|
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) {
|
} catch (err: any) {
|
||||||
if (err.response?.status === 403) {
|
if (err.response?.status === 403) {
|
||||||
console.warn(
|
console.error(
|
||||||
` ⚠️ Cleanup fetch failed with 403 Forbidden. This is expected if the runner lacks admin API credentials. Test submissions remain in the database.`,
|
` ❌ Cleanup failed with 403 Forbidden. Ensure FormSubmissions access control and PAYLOAD_SECRET are aligned.`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error(` ❌ Cleanup fetch failed: ${err.message}`);
|
console.error(` ❌ Cleanup fetch failed: ${err.message}`);
|
||||||
}
|
}
|
||||||
// Don't mark the whole test as failed just because cleanup failed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|||||||
@@ -9,9 +9,19 @@ export const FormSubmissions: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
// Only Admins can view and delete leads via dashboard.
|
// Only Admins can view and delete leads via dashboard.
|
||||||
read: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
|
// E2E scripts can bypass if they provide the correct secret header.
|
||||||
update: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
|
read: ({ req }) => {
|
||||||
delete: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
|
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.
|
// Next.js server actions handle secure inserts natively. No public client create access.
|
||||||
create: () => false,
|
create: () => false,
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
test-render.pdf
BIN
test-render.pdf
Binary file not shown.
Reference in New Issue
Block a user