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
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 57s
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -306,7 +306,8 @@ const formData = ref({
|
||||
const visibility = ref({
|
||||
showEmail: true,
|
||||
showPhone: true,
|
||||
showAddress: false
|
||||
showAddress: false,
|
||||
showBirthday: true
|
||||
})
|
||||
|
||||
const passwordData = ref({
|
||||
|
||||
@@ -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: ''
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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.'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user