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
|
<input
|
||||||
v-model="formData.geburtsdatum"
|
v-model="formData.geburtsdatum"
|
||||||
type="date"
|
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"
|
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"
|
:disabled="isSaving"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -811,6 +811,10 @@ const canViewContactData = computed(() => {
|
|||||||
return authStore.hasRole('vorstand')
|
return authStore.hasRole('vorstand')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isBirthdateRequired = computed(() => {
|
||||||
|
return !editingMember.value || Boolean(editingMember.value?.geburtsdatum)
|
||||||
|
})
|
||||||
|
|
||||||
const filteredMembers = computed(() => {
|
const filteredMembers = computed(() => {
|
||||||
if (!filterHasHallKey.value) return members.value
|
if (!filterHasHallKey.value) return members.value
|
||||||
return members.value.filter(member => member.hasHallKey)
|
return members.value.filter(member => member.hasHallKey)
|
||||||
|
|||||||
@@ -472,12 +472,12 @@
|
|||||||
<input
|
<input
|
||||||
v-model="formData.geburtsdatum"
|
v-model="formData.geburtsdatum"
|
||||||
type="date"
|
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"
|
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"
|
:disabled="isSaving"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -937,6 +937,10 @@ const canViewContactData = computed(() => {
|
|||||||
return authStore.hasRole('vorstand')
|
return authStore.hasRole('vorstand')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isBirthdateRequired = computed(() => {
|
||||||
|
return !editingMember.value || Boolean(editingMember.value?.geburtsdatum)
|
||||||
|
})
|
||||||
|
|
||||||
const loadMembers = async () => {
|
const loadMembers = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -306,7 +306,8 @@ const formData = ref({
|
|||||||
const visibility = ref({
|
const visibility = ref({
|
||||||
showEmail: true,
|
showEmail: true,
|
||||||
showPhone: true,
|
showPhone: true,
|
||||||
showAddress: false
|
showAddress: false,
|
||||||
|
showBirthday: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const passwordData = ref({
|
const passwordData = ref({
|
||||||
|
|||||||
@@ -106,6 +106,31 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</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 -->
|
<!-- Password -->
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
@@ -340,6 +365,8 @@ const formData = ref({
|
|||||||
lastName: '',
|
lastName: '',
|
||||||
email: '',
|
email: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
|
geburtsdatum: '',
|
||||||
|
hideBirthday: false,
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: ''
|
confirmPassword: ''
|
||||||
})
|
})
|
||||||
@@ -424,6 +451,10 @@ const handleRegister = async () => {
|
|||||||
name: `${formData.value.firstName} ${formData.value.lastName}`.trim(),
|
name: `${formData.value.firstName} ${formData.value.lastName}`.trim(),
|
||||||
email: formData.value.email,
|
email: formData.value.email,
|
||||||
phone: formData.value.phone,
|
phone: formData.value.phone,
|
||||||
|
geburtsdatum: formData.value.geburtsdatum,
|
||||||
|
visibility: {
|
||||||
|
showBirthday: !formData.value.hideBirthday
|
||||||
|
},
|
||||||
password: formData.value.password
|
password: formData.value.password
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -437,6 +468,8 @@ const handleRegister = async () => {
|
|||||||
lastName: '',
|
lastName: '',
|
||||||
email: '',
|
email: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
|
geburtsdatum: '',
|
||||||
|
hideBirthday: false,
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: ''
|
confirmPassword: ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { assertPasswordNotPwned } from '../../utils/hibp.js'
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const body = await readBody(event)
|
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({
|
throw createError({
|
||||||
statusCode: 400,
|
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,
|
password: hashedPassword,
|
||||||
name,
|
name,
|
||||||
phone: phone || '',
|
phone: phone || '',
|
||||||
|
geburtsdatum,
|
||||||
|
visibility: {
|
||||||
|
showBirthday: visibility?.showBirthday !== undefined ? Boolean(visibility.showBirthday) : true
|
||||||
|
},
|
||||||
role: 'mitglied',
|
role: 'mitglied',
|
||||||
active: false, // Requires admin approval
|
active: false, // Requires admin approval
|
||||||
created: new Date().toISOString(),
|
created: new Date().toISOString(),
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
loginRole: roles[0] || 'mitglied', // Rückwärtskompatibilität
|
loginRole: roles[0] || 'mitglied', // Rückwärtskompatibilität
|
||||||
lastLogin: user.lastLogin,
|
lastLogin: user.lastLogin,
|
||||||
isMannschaftsspieler: user.isMannschaftsspieler === true || mergedMembers[matchedManualIndex].isMannschaftsspieler === true,
|
isMannschaftsspieler: user.isMannschaftsspieler === true || mergedMembers[matchedManualIndex].isMannschaftsspieler === true,
|
||||||
|
geburtsdatum: mergedMembers[matchedManualIndex].geburtsdatum || user.geburtsdatum || '',
|
||||||
firstName: mergedMembers[matchedManualIndex].firstName || firstName,
|
firstName: mergedMembers[matchedManualIndex].firstName || firstName,
|
||||||
lastName: mergedMembers[matchedManualIndex].lastName || lastName,
|
lastName: mergedMembers[matchedManualIndex].lastName || lastName,
|
||||||
editable: true
|
editable: true
|
||||||
@@ -176,6 +177,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
geburtsdatum: user.geburtsdatum || '',
|
||||||
email: user.email,
|
email: user.email,
|
||||||
phone: user.phone || '',
|
phone: user.phone || '',
|
||||||
address: '',
|
address: '',
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!geburtsdatum) {
|
if (!geburtsdatum && !id) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
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 () => {
|
it('verhindert doppelte Benutzer', async () => {
|
||||||
const event = createEvent()
|
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' }])
|
authUtils.readUsers.mockResolvedValue([{ email: 'max@example.com' }])
|
||||||
|
|
||||||
await expect(registerHandler(event)).rejects.toMatchObject({ statusCode: 409 })
|
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 () => {
|
it('legt Benutzer an und versendet E-Mails', async () => {
|
||||||
const event = createEvent()
|
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.readUsers.mockResolvedValue([])
|
||||||
authUtils.hashPassword.mockResolvedValue('hashed')
|
authUtils.hashPassword.mockResolvedValue('hashed')
|
||||||
authUtils.writeUsers.mockResolvedValue(true)
|
authUtils.writeUsers.mockResolvedValue(true)
|
||||||
@@ -151,6 +165,10 @@ describe('Auth API Endpoints', () => {
|
|||||||
|
|
||||||
expect(response.success).toBe(true)
|
expect(response.success).toBe(true)
|
||||||
expect(authUtils.writeUsers).toHaveBeenCalled()
|
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()
|
expect(nodemailer.default.createTransport).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { createEvent, mockSuccessReadBody } from './setup'
|
import { createEvent, mockSuccessReadBody } from './setup'
|
||||||
|
|
||||||
@@ -150,6 +151,25 @@ describe('Members API Endpoints', () => {
|
|||||||
const response = await membersPostHandler(event)
|
const response = await membersPostHandler(event)
|
||||||
expect(response.success).toBe(true)
|
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', () => {
|
describe('DELETE /api/members', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user