fix(andreknie.de): improve mobile dock and resource requests #3
@@ -112,4 +112,21 @@ describe('backend mail safety', () => {
|
|||||||
expect(response.status).toBe(400)
|
expect(response.status).toBe(400)
|
||||||
expect((await response.json()).errors.resource_id).toBeTruthy()
|
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 = {}
|
const errors = {}
|
||||||
if (!name || name.trim().length === 0) errors.name = 'Name ist erforderlich.'
|
if (!name || name.trim().length === 0) errors.name = 'Name ist erforderlich.'
|
||||||
if (name && name.length > 100) errors.name = 'Max. 100 Zeichen.'
|
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.trim().length === 0) errors.company = 'Unternehmen ist erforderlich.'
|
||||||
if (company && company.length > 150) errors.company = 'Max. 150 Zeichen.'
|
if (company && company.length > 150) errors.company = 'Max. 150 Zeichen.'
|
||||||
if (!resource_id || !getAvailableResourceIds().includes(resource_id)) {
|
if (!resource_id || !getAvailableResourceIds().includes(resource_id)) {
|
||||||
|
|||||||
@@ -62,11 +62,7 @@ export async function sendStakeholderNotification(type, data) {
|
|||||||
'resource-download': `Ressourcenanfrage von ${data.name}`,
|
'resource-download': `Ressourcenanfrage von ${data.name}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = [
|
const body = buildStakeholderMessage(type, data)
|
||||||
'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')
|
|
||||||
|
|
||||||
await t.sendMail({
|
await t.sendMail({
|
||||||
from: SMTP_USER,
|
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 }
|
export { STAKEHOLDER_EMAIL, BASE_URL }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { isMailerReady, MailerNotReadyError, sendConfirmationEmail } from './mailer.js'
|
import {
|
||||||
|
buildStakeholderMessage,
|
||||||
|
isMailerReady,
|
||||||
|
MailerNotReadyError,
|
||||||
|
sendConfirmationEmail,
|
||||||
|
} from './mailer.js'
|
||||||
|
|
||||||
describe('mailer readiness', () => {
|
describe('mailer readiness', () => {
|
||||||
it('reports missing SMTP configuration without exposing credentials', async () => {
|
it('reports missing SMTP configuration without exposing credentials', async () => {
|
||||||
@@ -10,3 +15,24 @@ describe('mailer readiness', () => {
|
|||||||
.rejects.toMatchObject({ code: 'MAILER_NOT_READY' })
|
.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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -14,8 +14,42 @@
|
|||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.hero {
|
.hero {
|
||||||
|
padding-top: 100px;
|
||||||
padding-bottom: calc(60px + var(--dock-content-space));
|
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 {
|
.hero-content {
|
||||||
|
|||||||
Reference in New Issue
Block a user