diff --git a/.gitignore b/.gitignore index 50ebe34..60f7054 100644 --- a/.gitignore +++ b/.gitignore @@ -138,4 +138,7 @@ Thumbs.db # Temporary files *.tmp -*.temp \ No newline at end of file +*.temp + +# Sensitive data (DO NOT commit production sessions!) +# server/data/sessions.json - uncomment for production \ No newline at end of file diff --git a/AUTH_README.md b/AUTH_README.md new file mode 100644 index 0000000..dc9cc45 --- /dev/null +++ b/AUTH_README.md @@ -0,0 +1,56 @@ +# 🔐 Authentifizierung & Mitgliederbereich + +## Standard-Login + +**E-Mail:** `admin@harheimertc.de` +**Passwort:** `admin123` + +⚠️ **WICHTIG:** Ändern Sie dieses Passwort sofort nach der ersten Anmeldung! + +## Passwort-Hash generieren + +Um einen neuen Benutzer oder ein neues Passwort zu erstellen, können Sie folgenden Node.js-Code verwenden: + +```javascript +import bcrypt from 'bcryptjs' + +const password = 'IhrNeuesPasswort' +const hash = await bcrypt.hash(password, 10) +console.log(hash) +``` + +Oder mit einem Online-Tool: https://bcrypt-generator.com/ (Rounds: 10) + +## Benutzerrollen + +- **admin**: Voller Zugriff auf CMS und Mitgliederbereich +- **vorstand**: Zugriff auf CMS und Mitgliederbereich +- **mitglied**: Nur Mitgliederbereich + +## Dateien + +- `server/data/users.json` - Benutzerdaten (verschlüsselte Passwörter) +- `server/data/members.json` - Mitgliederdaten (Telefon, E-Mail, etc.) +- `server/data/sessions.json` - Aktive Sessions + +## Sicherheit + +- Passwörter werden mit bcrypt gehasht (Rounds: 10) +- JWT-Tokens für Sessions (7 Tage gültig) +- HTTP-Only Cookies +- Geschützte API-Routen +- Middleware für geschützte Seiten + +## Environment Variables + +Fügen Sie in `.env` hinzu: + +``` +JWT_SECRET=ihr-geheimer-jwt-schluessel-hier +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=ihre-email@example.com +SMTP_PASS=ihr-smtp-passwort +SMTP_FROM=noreply@harheimertc.de +``` + diff --git a/components/Footer.vue b/components/Footer.vue index 018406d..86c0959 100644 --- a/components/Footer.vue +++ b/components/Footer.vue @@ -5,13 +5,73 @@

Š {{ currentYear }} Harheimer TC

-
+
Impressum Kontakt + + +
+ + + + +
+ + +
+
+
@@ -19,5 +79,57 @@ diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..fc32a85 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,29 @@ +export default defineNuxtRouteMiddleware(async (to, from) => { + // Check if route requires auth + const protectedRoutes = ['/mitgliederbereich', '/cms'] + const requiresAuth = protectedRoutes.some(route => to.path.startsWith(route)) + + if (!requiresAuth) { + return + } + + // Check auth status + try { + const { data: auth } = await useFetch('/api/auth/status') + + if (!auth.value || !auth.value.isLoggedIn) { + return navigateTo('/login?redirect=' + to.path) + } + + // Check role for CMS + if (to.path.startsWith('/cms')) { + const isAdmin = auth.value.role === 'admin' || auth.value.role === 'vorstand' + if (!isAdmin) { + return navigateTo('/mitgliederbereich') + } + } + } catch (error) { + return navigateTo('/login?redirect=' + to.path) + } +}) + diff --git a/package.json b/package.json index c8d45d6..27da45e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "postinstall": "nuxt prepare" }, "dependencies": { + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", "nodemailer": "^7.0.9", "nuxt": "^3.11.0", "vue": "^3.4.0" diff --git a/pages/cms/index.vue b/pages/cms/index.vue new file mode 100644 index 0000000..3bfee47 --- /dev/null +++ b/pages/cms/index.vue @@ -0,0 +1,124 @@ + + + + diff --git a/pages/login.vue b/pages/login.vue new file mode 100644 index 0000000..e87fe6b --- /dev/null +++ b/pages/login.vue @@ -0,0 +1,152 @@ + + + + diff --git a/pages/mitgliederbereich/index.vue b/pages/mitgliederbereich/index.vue new file mode 100644 index 0000000..2a2f3ca --- /dev/null +++ b/pages/mitgliederbereich/index.vue @@ -0,0 +1,111 @@ + + + + diff --git a/pages/passwort-vergessen.vue b/pages/passwort-vergessen.vue new file mode 100644 index 0000000..d48e6db --- /dev/null +++ b/pages/passwort-vergessen.vue @@ -0,0 +1,115 @@ + + + + diff --git a/server/api/auth/login.post.js b/server/api/auth/login.post.js new file mode 100644 index 0000000..c45625c --- /dev/null +++ b/server/api/auth/login.post.js @@ -0,0 +1,69 @@ +import { readUsers, writeUsers, verifyPassword, generateToken, createSession } from '../../utils/auth.js' + +export default defineEventHandler(async (event) => { + try { + const body = await readBody(event) + const { email, password } = body + + if (!email || !password) { + throw createError({ + statusCode: 400, + message: 'E-Mail und Passwort sind erforderlich' + }) + } + + // Find user + const users = await readUsers() + const user = users.find(u => u.email.toLowerCase() === email.toLowerCase()) + + if (!user) { + throw createError({ + statusCode: 401, + message: 'UngĂźltige Anmeldedaten' + }) + } + + // Verify password + const isValid = await verifyPassword(password, user.password) + if (!isValid) { + throw createError({ + statusCode: 401, + message: 'UngĂźltige Anmeldedaten' + }) + } + + // Generate token + const token = generateToken(user) + + // Create session + await createSession(user.id, token) + + // Update last login + user.lastLogin = new Date().toISOString() + const updatedUsers = users.map(u => u.id === user.id ? user : u) + await writeUsers(updatedUsers) + + // Set cookie + setCookie(event, 'auth_token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7 // 7 days + }) + + // Return user data (without password) + return { + success: true, + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role + } + } + } catch (error) { + console.error('Login-Fehler:', error) + throw error + } +}) + diff --git a/server/api/auth/logout.post.js b/server/api/auth/logout.post.js new file mode 100644 index 0000000..e945f3e --- /dev/null +++ b/server/api/auth/logout.post.js @@ -0,0 +1,26 @@ +import { deleteSession } from '../../utils/auth.js' + +export default defineEventHandler(async (event) => { + try { + const token = getCookie(event, 'auth_token') + + if (token) { + await deleteSession(token) + } + + // Delete cookie + deleteCookie(event, 'auth_token') + + return { + success: true, + message: 'Erfolgreich abgemeldet' + } + } catch (error) { + console.error('Logout-Fehler:', error) + throw createError({ + statusCode: 500, + message: 'Abmeldung fehlgeschlagen' + }) + } +}) + diff --git a/server/api/auth/reset-password.post.js b/server/api/auth/reset-password.post.js new file mode 100644 index 0000000..d15134a --- /dev/null +++ b/server/api/auth/reset-password.post.js @@ -0,0 +1,82 @@ +import { readUsers, hashPassword, writeUsers } from '../../utils/auth.js' +import nodemailer from 'nodemailer' +import crypto from 'crypto' + +export default defineEventHandler(async (event) => { + try { + const body = await readBody(event) + const { email } = body + + if (!email) { + throw createError({ + statusCode: 400, + message: 'E-Mail-Adresse ist erforderlich' + }) + } + + // Find user + const users = await readUsers() + const user = users.find(u => u.email.toLowerCase() === email.toLowerCase()) + + // Always return success (security: don't reveal if email exists) + if (!user) { + return { + success: true, + message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' + } + } + + // Generate temporary password + const tempPassword = crypto.randomBytes(8).toString('hex') + const hashedPassword = await hashPassword(tempPassword) + + // Update user password + user.password = hashedPassword + user.passwordResetRequired = true + const updatedUsers = users.map(u => u.id === user.id ? user : u) + await writeUsers(updatedUsers) + + // Send email with temporary password + const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST || 'smtp.gmail.com', + port: process.env.SMTP_PORT || 587, + secure: false, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + } + }) + + const mailOptions = { + from: process.env.SMTP_FROM || 'noreply@harheimertc.de', + to: user.email, + subject: 'Passwort zurĂźcksetzen - Harheimer TC', + html: ` +

Passwort zurĂźcksetzen

+

Hallo ${user.name},

+

Sie haben eine Anfrage zum ZurĂźcksetzen Ihres Passworts gestellt.

+

Ihr temporäres Passwort lautet: ${tempPassword}

+

Bitte melden Sie sich damit an und ändern Sie Ihr Passwort im Mitgliederbereich.

+
+

Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail.

+
+

Mit sportlichen Grüßen,
Ihr Harheimer TC

+ ` + } + + await transporter.sendMail(mailOptions) + + return { + success: true, + message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' + } + } catch (error) { + console.error('Password-Reset-Fehler:', error) + // Don't reveal errors to prevent email enumeration + return { + success: true, + message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' + } + } +}) + diff --git a/server/api/auth/status.get.js b/server/api/auth/status.get.js new file mode 100644 index 0000000..3a5cdd8 --- /dev/null +++ b/server/api/auth/status.get.js @@ -0,0 +1,45 @@ +import { getUserFromToken } from '../../utils/auth.js' + +export default defineEventHandler(async (event) => { + try { + const token = getCookie(event, 'auth_token') + + if (!token) { + return { + isLoggedIn: false, + user: null, + role: null + } + } + + const user = await getUserFromToken(token) + + if (!user) { + deleteCookie(event, 'auth_token') + return { + isLoggedIn: false, + user: null, + role: null + } + } + + return { + isLoggedIn: true, + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role + }, + role: user.role + } + } catch (error) { + console.error('Auth-Status-Fehler:', error) + return { + isLoggedIn: false, + user: null, + role: null + } + } +}) + diff --git a/server/data/members.json b/server/data/members.json new file mode 100644 index 0000000..62773ed --- /dev/null +++ b/server/data/members.json @@ -0,0 +1,12 @@ +[ + { + "id": "1", + "name": "Admin", + "email": "admin@harheimertc.de", + "phone": "069-12345678", + "role": "admin", + "memberSince": "2020-01-01", + "active": true + } +] + diff --git a/server/data/sessions.json b/server/data/sessions.json new file mode 100644 index 0000000..7dd4387 --- /dev/null +++ b/server/data/sessions.json @@ -0,0 +1,2 @@ +[] + diff --git a/server/data/users.json b/server/data/users.json new file mode 100644 index 0000000..6893fd3 --- /dev/null +++ b/server/data/users.json @@ -0,0 +1,13 @@ +[ + { + "id": "1", + "email": "admin@harheimertc.de", + "password": "$2a$10$rKqW8x3k5vJ8pZ7mN9qL1OXxYzQ2wF3bH4cT6nR8sV9kL0mP1qW2e", + "name": "Admin", + "role": "admin", + "phone": "", + "created": "2025-10-21T00:00:00.000Z", + "lastLogin": null + } +] + diff --git a/server/utils/auth.js b/server/utils/auth.js new file mode 100644 index 0000000..9e1611f --- /dev/null +++ b/server/utils/auth.js @@ -0,0 +1,125 @@ +import bcrypt from 'bcryptjs' +import jwt from 'jsonwebtoken' +import { promises as fs } from 'fs' +import path from 'path' + +const JWT_SECRET = process.env.JWT_SECRET || 'harheimertc-secret-key-change-in-production' +const USERS_FILE = path.join(process.cwd(), 'server/data/users.json') +const SESSIONS_FILE = path.join(process.cwd(), 'server/data/sessions.json') + +// Read users from file +export async function readUsers() { + try { + const data = await fs.readFile(USERS_FILE, 'utf-8') + return JSON.parse(data) + } catch (error) { + console.error('Fehler beim Lesen der Benutzerdaten:', error) + return [] + } +} + +// Write users to file +export async function writeUsers(users) { + try { + await fs.writeFile(USERS_FILE, JSON.stringify(users, null, 2), 'utf-8') + return true + } catch (error) { + console.error('Fehler beim Schreiben der Benutzerdaten:', error) + return false + } +} + +// Read sessions from file +export async function readSessions() { + try { + const data = await fs.readFile(SESSIONS_FILE, 'utf-8') + return JSON.parse(data) + } catch (error) { + console.error('Fehler beim Lesen der Sessions:', error) + return [] + } +} + +// Write sessions to file +export async function writeSessions(sessions) { + try { + await fs.writeFile(SESSIONS_FILE, JSON.stringify(sessions, null, 2), 'utf-8') + return true + } catch (error) { + console.error('Fehler beim Schreiben der Sessions:', error) + return false + } +} + +// Hash password +export async function hashPassword(password) { + const salt = await bcrypt.genSalt(10) + return await bcrypt.hash(password, salt) +} + +// Verify password +export async function verifyPassword(password, hash) { + return await bcrypt.compare(password, hash) +} + +// Generate JWT token +export function generateToken(user) { + return jwt.sign( + { + id: user.id, + email: user.email, + role: user.role + }, + JWT_SECRET, + { expiresIn: '7d' } + ) +} + +// Verify JWT token +export function verifyToken(token) { + try { + return jwt.verify(token, JWT_SECRET) + } catch (error) { + return null + } +} + +// Get user from token +export async function getUserFromToken(token) { + const decoded = verifyToken(token) + if (!decoded) return null + + const users = await readUsers() + return users.find(u => u.id === decoded.id) +} + +// Create session +export async function createSession(userId, token) { + const sessions = await readSessions() + const session = { + id: Date.now().toString(), + userId, + token, + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 7 days + } + sessions.push(session) + await writeSessions(sessions) + return session +} + +// Delete session +export async function deleteSession(token) { + const sessions = await readSessions() + const filtered = sessions.filter(s => s.token !== token) + await writeSessions(filtered) +} + +// Clean expired sessions +export async function cleanExpiredSessions() { + const sessions = await readSessions() + const now = new Date() + const valid = sessions.filter(s => new Date(s.expiresAt) > now) + await writeSessions(valid) +} +