feat: Add CMS and Member Area screens with ViewModels
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 5m23s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m18s

- Implemented CmsViewModel to manage CMS data loading and state.
- Created MemberAreaDetailScreens for displaying member information and news.
- Added MembersViewModel and MemberNewsViewModel for managing member data and news.
- Developed MemberAreaScreen to provide navigation and display member-related options.
- Introduced ProfileScreen and ProfileViewModel for user profile management.
- Implemented state management for loading, error handling, and form updates across screens.
This commit is contained in:
Torsten Schulz (local)
2026-05-28 08:01:35 +02:00
parent e195d5d189
commit e033d716dd
34 changed files with 1809 additions and 72 deletions

View File

@@ -1,5 +1,5 @@
import { readMembers, normalizeDate } from '../utils/members.js'
import { readUsers, migrateUserRoles, getUserFromToken, verifyToken } from '../utils/auth.js'
import { readMembers } from '../utils/members.js'
import { readUsers, getUserFromToken, verifyToken } from '../utils/auth.js'
// Helper: returns array of upcoming birthdays within daysAhead (inclusive)
function getUpcomingBirthdays(entries, daysAhead = 28) {
@@ -41,7 +41,7 @@ function getUpcomingBirthdays(entries, daysAhead = 28) {
export default defineEventHandler(async (event) => {
try {
// Determine viewer for visibility rules; token optional
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
let currentUser = null
if (token) {
const decoded = verifyToken(token)

View File

@@ -2,7 +2,7 @@ import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
import { readContactRequests } from '../../utils/contact-requests.js'
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
const currentUser = token ? await getUserFromToken(token) : null
if (!currentUser || !hasAnyRole(currentUser, 'admin', 'vorstand', 'trainer')) {

View File

@@ -48,7 +48,7 @@ function summarizeAttempts(entries) {
}
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
const currentUser = token ? await getUserFromToken(token) : null
if (!currentUser || !hasRole(currentUser, 'admin')) {

View File

@@ -1,8 +1,8 @@
import { getUserFromToken, readUsers, hasAnyRole, hasRole, migrateUserRoles } from '../../../utils/auth.js'
import { getUserFromToken, readUsers, hasAnyRole, migrateUserRoles } from '../../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
const currentUser = await getUserFromToken(token)
// Seite darf von Admin ODER Vorstand genutzt werden
@@ -15,8 +15,6 @@ export default defineEventHandler(async (event) => {
const users = await readUsers()
const isVorstand = hasRole(currentUser, 'vorstand')
// Nur Admin oder Vorstand duerfen vollen Benutzer-Contact und Rollen sehen.
const canSeePrivate = hasAnyRole(currentUser, 'admin', 'vorstand')
@@ -53,4 +51,3 @@ export default defineEventHandler(async (event) => {
throw error
}
})

View File

@@ -16,7 +16,7 @@ const getDataPath = (filename) => {
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
if (!token) {
throw createError({
@@ -58,4 +58,3 @@ export default defineEventHandler(async (event) => {
throw error
}
})

View File

@@ -4,7 +4,7 @@ import { readUsers, migrateUserRoles } from '../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
if (!token) {
throw createError({
@@ -203,7 +203,6 @@ export default defineEventHandler(async (event) => {
mergedMembers.sort((a, b) => a.name.localeCompare(b.name))
// Die Mitgliederliste ist nur für authentifizierte Nutzer sichtbar (siehe oben).
// Respektiere individuelle Sichtbarkeitspräferenzen (user.visibility)
const currentUserToken = token
const isViewerAuthenticated = !!currentUser
// Only 'vorstand' may override member visibility
const isPrivilegedViewer = currentUser ? hasRole(currentUser, 'vorstand') : false
@@ -218,16 +217,11 @@ export default defineEventHandler(async (event) => {
const showPhone = visibility.showPhone === undefined ? true : Boolean(visibility.showPhone)
const showAddress = visibility.showAddress === undefined ? false : Boolean(visibility.showAddress)
// Determine if contact info existed but was hidden to the viewer
const hadEmail = !!member.email
const hadPhone = !!member.phone
const hadAddress = !!member.address
const hadBirthday = !!member.geburtsdatum
const emailVisible = (isPrivilegedViewer || (isViewerAuthenticated && showEmail))
const phoneVisible = (isPrivilegedViewer || (isViewerAuthenticated && showPhone))
const addressVisible = (isPrivilegedViewer || (isViewerAuthenticated && showAddress))
const birthdayVisible = (isPrivilegedViewer || (isViewerAuthenticated && (member.visibility && member.visibility.showBirthday !== undefined ? Boolean(member.visibility.showBirthday) : true)))
const contactHidden = (!emailVisible && hadEmail) || (!phoneVisible && hadPhone) || (!addressVisible && hadAddress)
return {
id: member.id,
@@ -260,7 +254,7 @@ export default defineEventHandler(async (event) => {
const day = `${d.getDate()}`.padStart(2, '0')
const month = `${d.getMonth()+1}`.padStart(2, '0')
return `${day}.${month}`
} catch (_e) {
} catch {
return undefined
}
})() : undefined,
@@ -277,4 +271,3 @@ export default defineEventHandler(async (event) => {
throw error
}
})

View File

@@ -3,7 +3,7 @@ import { deleteNews } from '../utils/news.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
if (!token) {
throw createError({
@@ -53,4 +53,3 @@ export default defineEventHandler(async (event) => {
throw error
}
})

View File

@@ -3,7 +3,7 @@ import { readNews } from '../utils/news.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
if (!token) {
throw createError({
@@ -35,4 +35,3 @@ export default defineEventHandler(async (event) => {
throw error
}
})

View File

@@ -3,7 +3,7 @@ import { saveNews } from '../utils/news.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
if (!token) {
throw createError({
@@ -60,4 +60,3 @@ export default defineEventHandler(async (event) => {
throw error
}
})

View File

@@ -2,7 +2,7 @@ import { verifyToken, getUserFromToken } from '../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
if (!token) {
throw createError({ statusCode: 401, message: 'Nicht authentifiziert.' })
@@ -27,7 +27,7 @@ export default defineEventHandler(async (event) => {
email: user.email,
phone: user.phone || '',
geburtsdatum: user.geburtsdatum || '',
visibility: Object.assign({ showBirthday: true }, (user.visibility || {}))
visibility: Object.assign({ showBirthday: true }, (user.visibility || {}))
}
}
} catch (error) {
@@ -35,4 +35,3 @@ export default defineEventHandler(async (event) => {
throw error
}
})

View File

@@ -1,9 +1,9 @@
import { verifyToken, readUsers, writeUsers, verifyPassword, hashPassword, migrateUserRoles, revokeRefreshSessionsForUser } from '../utils/auth.js'
import { verifyToken, getUserFromToken, readUsers, writeUsers, verifyPassword, hashPassword, migrateUserRoles, revokeRefreshSessionsForUser } from '../utils/auth.js'
import { assertPasswordNotPwned } from '../utils/hibp.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
if (!token) {
throw createError({
@@ -21,6 +21,16 @@ export default defineEventHandler(async (event) => {
})
}
if (decoded.sid) {
const sessionUser = await getUserFromToken(token)
if (!sessionUser) {
throw createError({
statusCode: 401,
message: 'Ungültige Sitzung.'
})
}
}
const body = await readBody(event)
const { name, email, phone, geburtsdatum, currentPassword, newPassword } = body
@@ -31,7 +41,7 @@ export default defineEventHandler(async (event) => {
})
}
const users = await readUsers()
const users = await readUsers()
const userIndex = users.findIndex(u => u.id === decoded.id)
if (userIndex === -1) {