fix(andreknie.de): close package two review gaps

This commit is contained in:
2026-07-28 21:13:21 +02:00
parent 8236cce85c
commit 0334252fed
5 changed files with 91 additions and 7 deletions
@@ -112,4 +112,21 @@ describe('backend mail safety', () => {
expect(response.status).toBe(400)
expect((await response.json()).errors.resource_id).toBeTruthy()
})
it('silently accepts resource honeypot submissions', async () => {
const response = await fetch(`${baseUrl}/api/resource-download`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
name: 'Test',
email: 'test@example.invalid',
company: 'Synthetic Test',
resource_id: 'does-not-exist',
company_website: 'https://bot.example.invalid',
}),
})
expect(response.status).toBe(201)
expect((await response.json()).ok).toBe(true)
})
})
@@ -23,7 +23,7 @@ router.post('/', resourceLimiter, antiSpam, async (req, res) => {
const errors = {}
if (!name || name.trim().length === 0) errors.name = 'Name ist erforderlich.'
if (name && name.length > 100) errors.name = 'Max. 100 Zeichen.'
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errors.email = 'Gueltige E-Mail erforderlich.'
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errors.email = 'Gültige E-Mail erforderlich.'
if (!company || company.trim().length === 0) errors.company = 'Unternehmen ist erforderlich.'
if (company && company.length > 150) errors.company = 'Max. 150 Zeichen.'
if (!resource_id || !getAvailableResourceIds().includes(resource_id)) {
@@ -62,11 +62,7 @@ export async function sendStakeholderNotification(type, data) {
'resource-download': `Ressourcenanfrage von ${data.name}`,
}
const body = [
'Bitte die angefragten Inhalte prüfen und bei positiver Prüfung ein aktuelles Profil von André verschicken.',
'',
...Object.entries(data).map(([key, val]) => `${key}: ${val}`),
].join('\n')
const body = buildStakeholderMessage(type, data)
await t.sendMail({
from: SMTP_USER,
@@ -76,6 +72,17 @@ export async function sendStakeholderNotification(type, data) {
})
}
export function buildStakeholderMessage(type, data) {
const fields = Object.entries(data).map(([key, val]) => `${key}: ${val}`)
if (type !== 'resource-download') return fields.join('\n')
return [
'Bitte die angefragten Inhalte prüfen und bei positiver Prüfung ein aktuelles Profil von André verschicken.',
'',
...fields,
].join('\n')
}
export { STAKEHOLDER_EMAIL, BASE_URL }
/**
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest'
import { isMailerReady, MailerNotReadyError, sendConfirmationEmail } from './mailer.js'
import {
buildStakeholderMessage,
isMailerReady,
MailerNotReadyError,
sendConfirmationEmail,
} from './mailer.js'
describe('mailer readiness', () => {
it('reports missing SMTP configuration without exposing credentials', async () => {
@@ -10,3 +15,24 @@ describe('mailer readiness', () => {
.rejects.toMatchObject({ code: 'MAILER_NOT_READY' })
})
})
describe('stakeholder notification content', () => {
const data = {
name: 'Synthetic Test',
email: 'test@example.invalid',
}
it('adds the manual profile instruction to resource requests', () => {
const message = buildStakeholderMessage('resource-download', data)
expect(message).toContain('ein aktuelles Profil von André verschicken')
expect(message).toContain('name: Synthetic Test')
})
it('does not add the resource instruction to contact notifications', () => {
const message = buildStakeholderMessage('contact', data)
expect(message).not.toContain('ein aktuelles Profil von André verschicken')
expect(message).toBe('name: Synthetic Test\nemail: test@example.invalid')
})
})
+34
View File
@@ -14,8 +14,42 @@
@media (max-width: 600px) {
.hero {
padding-top: 100px;
padding-bottom: calc(60px + var(--dock-content-space));
}
.hero-photo {
width: 140px;
height: 140px;
margin-bottom: 16px;
}
.hero h1 {
font-size: 2.25rem;
margin-bottom: 8px;
}
.hero-roles {
font-size: 1rem;
margin-bottom: 12px;
}
.hero-subtext {
font-size: 1rem;
line-height: 1.45;
margin-bottom: 24px;
}
.hero-actions {
flex-wrap: nowrap;
gap: 10px;
}
.hero-actions .primary-button,
.hero-actions .secondary-button {
padding: 12px 18px;
font-size: 0.95rem;
}
}
.hero-content {