Add birthdate handling in member registration and management. Update UI to conditionally require birthdate for new members, and enhance API to enforce birthdate validation. Improve tests to cover new birthdate requirements.
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 57s

This commit is contained in:
Torsten Schulz (local)
2026-03-31 07:25:44 +02:00
parent 8ffd267dfc
commit 0fb58af194
9 changed files with 98 additions and 12 deletions

View File

@@ -469,12 +469,12 @@
<input
v-model="formData.geburtsdatum"
type="date"
required
:required="isBirthdateRequired"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
>
<p class="text-xs text-gray-500 mt-1">
Wird zur eindeutigen Identifizierung benötigt
Fuer neue Mitglieder erforderlich. Altdaten ohne Geburtsdatum koennen weiter bearbeitet werden.
</p>
</div>
@@ -811,6 +811,10 @@ const canViewContactData = computed(() => {
return authStore.hasRole('vorstand')
})
const isBirthdateRequired = computed(() => {
return !editingMember.value || Boolean(editingMember.value?.geburtsdatum)
})
const filteredMembers = computed(() => {
if (!filterHasHallKey.value) return members.value
return members.value.filter(member => member.hasHallKey)

View File

@@ -472,12 +472,12 @@
<input
v-model="formData.geburtsdatum"
type="date"
required
:required="isBirthdateRequired"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
>
<p class="text-xs text-gray-500 mt-1">
Wird zur eindeutigen Identifizierung benötigt
Fuer neue Mitglieder erforderlich. Altdaten ohne Geburtsdatum koennen weiter bearbeitet werden.
</p>
</div>
@@ -937,6 +937,10 @@ const canViewContactData = computed(() => {
return authStore.hasRole('vorstand')
})
const isBirthdateRequired = computed(() => {
return !editingMember.value || Boolean(editingMember.value?.geburtsdatum)
})
const loadMembers = async () => {
isLoading.value = true
try {

View File

@@ -306,7 +306,8 @@ const formData = ref({
const visibility = ref({
showEmail: true,
showPhone: true,
showAddress: false
showAddress: false,
showBirthday: true
})
const passwordData = ref({

View File

@@ -106,6 +106,31 @@
>
</div>
<div>
<label
for="geburtsdatum"
class="block text-sm font-medium text-gray-700 mb-2"
>
Geburtsdatum
</label>
<input
id="geburtsdatum"
v-model="formData.geburtsdatum"
type="date"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-600 focus:border-transparent transition-all"
>
</div>
<label class="flex items-center gap-2 text-sm text-gray-700">
<input
v-model="formData.hideBirthday"
type="checkbox"
class="h-4 w-4 text-primary-600 focus:ring-primary-600 border-gray-300 rounded"
>
Geburtsdatum in der Mitgliederliste nicht anzeigen
</label>
<!-- Password -->
<div>
<label
@@ -340,6 +365,8 @@ const formData = ref({
lastName: '',
email: '',
phone: '',
geburtsdatum: '',
hideBirthday: false,
password: '',
confirmPassword: ''
})
@@ -424,6 +451,10 @@ const handleRegister = async () => {
name: `${formData.value.firstName} ${formData.value.lastName}`.trim(),
email: formData.value.email,
phone: formData.value.phone,
geburtsdatum: formData.value.geburtsdatum,
visibility: {
showBirthday: !formData.value.hideBirthday
},
password: formData.value.password
}
})
@@ -437,6 +468,8 @@ const handleRegister = async () => {
lastName: '',
email: '',
phone: '',
geburtsdatum: '',
hideBirthday: false,
password: '',
confirmPassword: ''
}

View File

@@ -5,12 +5,12 @@ import { assertPasswordNotPwned } from '../../utils/hibp.js'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const { name, email, phone, password } = body
const { name, email, phone, password, geburtsdatum, visibility } = body
if (!name || !email || !password) {
if (!name || !email || !password || !geburtsdatum) {
throw createError({
statusCode: 400,
message: 'Name, E-Mail und Passwort sind erforderlich'
message: 'Name, E-Mail, Geburtsdatum und Passwort sind erforderlich'
})
}
@@ -46,6 +46,10 @@ export default defineEventHandler(async (event) => {
password: hashedPassword,
name,
phone: phone || '',
geburtsdatum,
visibility: {
showBirthday: visibility?.showBirthday !== undefined ? Boolean(visibility.showBirthday) : true
},
role: 'mitglied',
active: false, // Requires admin approval
created: new Date().toISOString(),

View File

@@ -150,6 +150,7 @@ export default defineEventHandler(async (event) => {
loginRole: roles[0] || 'mitglied', // Rückwärtskompatibilität
lastLogin: user.lastLogin,
isMannschaftsspieler: user.isMannschaftsspieler === true || mergedMembers[matchedManualIndex].isMannschaftsspieler === true,
geburtsdatum: mergedMembers[matchedManualIndex].geburtsdatum || user.geburtsdatum || '',
firstName: mergedMembers[matchedManualIndex].firstName || firstName,
lastName: mergedMembers[matchedManualIndex].lastName || lastName,
editable: true
@@ -176,6 +177,7 @@ export default defineEventHandler(async (event) => {
name: user.name,
firstName,
lastName,
geburtsdatum: user.geburtsdatum || '',
email: user.email,
phone: user.phone || '',
address: '',

View File

@@ -48,10 +48,10 @@ export default defineEventHandler(async (event) => {
})
}
if (!geburtsdatum) {
if (!geburtsdatum && !id) {
throw createError({
statusCode: 400,
message: 'Geburtsdatum ist erforderlich, um Duplikate zu vermeiden.'
message: 'Geburtsdatum ist fuer neue Mitglieder erforderlich, um Duplikate zu vermeiden.'
})
}

View File

@@ -134,15 +134,29 @@ describe('Auth API Endpoints', () => {
it('verhindert doppelte Benutzer', async () => {
const event = createEvent()
mockSuccessReadBody({ name: 'Max', email: 'max@example.com', password: '12345678' })
mockSuccessReadBody({ name: 'Max', email: 'max@example.com', password: '12345678', geburtsdatum: '2000-01-01' })
authUtils.readUsers.mockResolvedValue([{ email: 'max@example.com' }])
await expect(registerHandler(event)).rejects.toMatchObject({ statusCode: 409 })
})
it('verlangt Geburtsdatum bei Registrierung', async () => {
const event = createEvent()
mockSuccessReadBody({ name: 'Max', email: 'max@example.com', password: '12345678' })
await expect(registerHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('legt Benutzer an und versendet E-Mails', async () => {
const event = createEvent()
mockSuccessReadBody({ name: 'Max', email: 'max@example.com', password: '12345678', phone: '123' })
mockSuccessReadBody({
name: 'Max',
email: 'max@example.com',
password: '12345678',
phone: '123',
geburtsdatum: '2000-01-01',
visibility: { showBirthday: false }
})
authUtils.readUsers.mockResolvedValue([])
authUtils.hashPassword.mockResolvedValue('hashed')
authUtils.writeUsers.mockResolvedValue(true)
@@ -151,6 +165,10 @@ describe('Auth API Endpoints', () => {
expect(response.success).toBe(true)
expect(authUtils.writeUsers).toHaveBeenCalled()
expect(authUtils.writeUsers.mock.calls[0][0][0]).toMatchObject({
geburtsdatum: '2000-01-01',
visibility: { showBirthday: false }
})
expect(nodemailer.default.createTransport).toHaveBeenCalled()
})
})

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createEvent, mockSuccessReadBody } from './setup'
@@ -150,6 +151,25 @@ describe('Members API Endpoints', () => {
const response = await membersPostHandler(event)
expect(response.success).toBe(true)
})
it('erlaubt Updates von Altdaten ohne Geburtsdatum', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
mockSuccessReadBody({
id: 'legacy-1',
firstName: 'Lisa',
lastName: 'Beispiel',
email: 'lisa@example.com'
})
authUtils.getUserFromToken.mockResolvedValue({ id: '3', role: 'vorstand' })
memberUtils.saveMember.mockResolvedValue(true)
const response = await membersPostHandler(event)
expect(response.success).toBe(true)
expect(memberUtils.saveMember).toHaveBeenCalledWith(expect.objectContaining({
id: 'legacy-1',
geburtsdatum: ''
}))
})
})
describe('DELETE /api/members', () => {