Respect per-user visibility; only 'vorstand' overrides visibility; UI shows contactHidden per-member
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 47s

This commit is contained in:
Torsten Schulz (local)
2026-02-11 13:24:01 +01:00
parent ce5915a3bc
commit 141a15a6cb
3 changed files with 49 additions and 65 deletions

View File

@@ -115,7 +115,10 @@
</div> </div>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap"> <td class="px-4 py-3 whitespace-nowrap">
<template v-if="canViewContactData"> <template v-if="member.contactHidden">
<span class="text-sm text-gray-400">Kontaktdaten nur für Vorstand sichtbar</span>
</template>
<template v-else>
<a <a
v-if="member.email" v-if="member.email"
:href="`mailto:${member.email}`" :href="`mailto:${member.email}`"
@@ -123,18 +126,14 @@
> >
{{ member.email }} {{ member.email }}
</a> </a>
<span <span v-else class="text-sm text-gray-400">-</span>
v-else
class="text-sm text-gray-400"
>-</span>
</template> </template>
<span
v-else
class="text-sm text-gray-400"
>Nur für Vorstand</span>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap"> <td class="px-4 py-3 whitespace-nowrap">
<template v-if="canViewContactData"> <template v-if="member.contactHidden">
<span class="text-sm text-gray-400">Kontaktdaten nur für Vorstand sichtbar</span>
</template>
<template v-else>
<a <a
v-if="member.phone" v-if="member.phone"
:href="`tel:${member.phone}`" :href="`tel:${member.phone}`"
@@ -142,15 +141,8 @@
> >
{{ member.phone }} {{ member.phone }}
</a> </a>
<span <span v-else class="text-sm text-gray-400">-</span>
v-else
class="text-sm text-gray-400"
>-</span>
</template> </template>
<span
v-else
class="text-sm text-gray-400"
>Nur für Vorstand</span>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap"> <td class="px-4 py-3 whitespace-nowrap">
<button <button
@@ -296,44 +288,22 @@
</div> </div>
<div class="grid sm:grid-cols-2 gap-3 text-gray-600"> <div class="grid sm:grid-cols-2 gap-3 text-gray-600">
<template v-if="canViewContactData"> <template v-if="member.contactHidden">
<div <div class="col-span-2 flex items-center text-gray-500 text-sm italic">
v-if="member.email" <Mail :size="16" class="mr-2" />
class="flex items-center" Kontaktdaten nur für Vorstand sichtbar
> </div>
<Mail </template>
:size="16" <template v-else>
class="mr-2 text-primary-600" <div v-if="member.email" class="flex items-center">
/> <Mail :size="16" class="mr-2 text-primary-600" />
<a <a :href="`mailto:${member.email}`" class="hover:text-primary-600">{{ member.email }}</a>
:href="`mailto:${member.email}`" </div>
class="hover:text-primary-600" <div v-if="member.phone" class="flex items-center">
>{{ member.email }}</a> <Phone :size="16" class="mr-2 text-primary-600" />
</div> <a :href="`tel:${member.phone}`" class="hover:text-primary-600">{{ member.phone }}</a>
<div
v-if="member.phone"
class="flex items-center"
>
<Phone
:size="16"
class="mr-2 text-primary-600"
/>
<a
:href="`tel:${member.phone}`"
class="hover:text-primary-600"
>{{ member.phone }}</a>
</div> </div>
</template> </template>
<div
v-else
class="col-span-2 flex items-center text-gray-500 text-sm italic"
>
<Mail
:size="16"
class="mr-2"
/>
Kontaktdaten nur für Vorstand sichtbar
</div>
<div <div
v-if="member.address" v-if="member.address"
class="flex items-start col-span-2" class="flex items-start col-span-2"

View File

@@ -22,7 +22,7 @@ export default defineEventHandler(async (event) => {
}) })
} }
const currentUser = await getUserFromToken(token) const currentUser = await getUserFromToken(token)
// Get manual members and registered users // Get manual members and registered users
const manualMembers = await readMembers() const manualMembers = await readMembers()
@@ -150,8 +150,10 @@ export default defineEventHandler(async (event) => {
mergedMembers.sort((a, b) => a.name.localeCompare(b.name)) mergedMembers.sort((a, b) => a.name.localeCompare(b.name))
// Die Mitgliederliste ist nur für authentifizierte Nutzer sichtbar (siehe oben). // Die Mitgliederliste ist nur für authentifizierte Nutzer sichtbar (siehe oben).
// Respektiere individuelle Sichtbarkeitspräferenzen (user.visibility) // Respektiere individuelle Sichtbarkeitspräferenzen (user.visibility)
const currentUserToken = token const currentUserToken = token
const isViewerAuthenticated = !!currentUser const isViewerAuthenticated = !!currentUser
// Only 'vorstand' may override member visibility
const isPrivilegedViewer = currentUser ? hasRole(currentUser, 'vorstand') : false
const sanitizedMembers = mergedMembers.map(member => { const sanitizedMembers = mergedMembers.map(member => {
// Default: show email/phone/address to other logged-in members unless member.visibility explicitly hides them // Default: show email/phone/address to other logged-in members unless member.visibility explicitly hides them
@@ -161,7 +163,16 @@ export default defineEventHandler(async (event) => {
const showPhone = visibility.showPhone === undefined ? true : Boolean(visibility.showPhone) const showPhone = visibility.showPhone === undefined ? true : Boolean(visibility.showPhone)
const showAddress = visibility.showAddress === undefined ? false : Boolean(visibility.showAddress) const showAddress = visibility.showAddress === undefined ? false : Boolean(visibility.showAddress)
return { // Determine if contact info existed but was hidden to the viewer
const hadEmail = !!member.email
const hadPhone = !!member.phone
const hadAddress = !!member.address
const emailVisible = (isPrivilegedViewer || (isViewerAuthenticated && showEmail))
const phoneVisible = (isPrivilegedViewer || (isViewerAuthenticated && showPhone))
const addressVisible = (isPrivilegedViewer || (isViewerAuthenticated && showAddress))
const contactHidden = (!emailVisible && hadEmail) || (!phoneVisible && hadPhone) || (!addressVisible && hadAddress)
return {
id: member.id, id: member.id,
name: member.name, name: member.name,
source: member.source, source: member.source,
@@ -172,10 +183,12 @@ export default defineEventHandler(async (event) => {
lastLogin: member.lastLogin, lastLogin: member.lastLogin,
isMannschaftsspieler: member.isMannschaftsspieler, isMannschaftsspieler: member.isMannschaftsspieler,
notes: member.notes || '', notes: member.notes || '',
// Only include contact fields when viewer is authenticated and the member allows it // Privileged viewers (vorstand) always see contact fields
email: (isViewerAuthenticated && showEmail) ? member.email : undefined, email: emailVisible ? member.email : undefined,
phone: (isViewerAuthenticated && showPhone) ? member.phone : undefined, phone: phoneVisible ? member.phone : undefined,
address: (isViewerAuthenticated && showAddress) ? member.address : undefined address: addressVisible ? member.address : undefined,
// Flag for UI: data existed but is hidden to the current viewer
contactHidden
} }
}) })

View File

@@ -64,9 +64,10 @@ export default defineEventHandler(async (event) => {
const visibility = body.visibility || body.visibilityPreferences || null const visibility = body.visibility || body.visibilityPreferences || null
if (visibility && typeof visibility === 'object') { if (visibility && typeof visibility === 'object') {
user.visibility = user.visibility || {} user.visibility = user.visibility || {}
if (typeof visibility.showEmail === 'boolean') user.visibility.showEmail = visibility.showEmail // Coerce values to booleans to be robust against string values from clients
if (typeof visibility.showPhone === 'boolean') user.visibility.showPhone = visibility.showPhone if (visibility.showEmail !== undefined) user.visibility.showEmail = Boolean(visibility.showEmail)
if (typeof visibility.showAddress === 'boolean') user.visibility.showAddress = visibility.showAddress if (visibility.showPhone !== undefined) user.visibility.showPhone = Boolean(visibility.showPhone)
if (visibility.showAddress !== undefined) user.visibility.showAddress = Boolean(visibility.showAddress)
} }
// Handle password change // Handle password change