Add authentication system with login, password reset, and member area
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -139,3 +139,6 @@ Thumbs.db
|
|||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
|
||||||
|
# Sensitive data (DO NOT commit production sessions!)
|
||||||
|
# server/data/sessions.json - uncomment for production
|
||||||
56
AUTH_README.md
Normal file
56
AUTH_README.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
@@ -5,13 +5,73 @@
|
|||||||
<p class="text-sm text-gray-400">
|
<p class="text-sm text-gray-400">
|
||||||
© {{ currentYear }} Harheimer TC
|
© {{ currentYear }} Harheimer TC
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center space-x-6 text-sm">
|
<div class="flex items-center space-x-6 text-sm relative">
|
||||||
<NuxtLink to="/impressum" class="text-gray-400 hover:text-primary-400 transition-colors">
|
<NuxtLink to="/impressum" class="text-gray-400 hover:text-primary-400 transition-colors">
|
||||||
Impressum
|
Impressum
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/kontakt" class="text-gray-400 hover:text-primary-400 transition-colors">
|
<NuxtLink to="/kontakt" class="text-gray-400 hover:text-primary-400 transition-colors">
|
||||||
Kontakt
|
Kontakt
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- Mitglieder Dropdown -->
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
@click="toggleMemberMenu"
|
||||||
|
class="flex items-center space-x-1 text-gray-400 hover:text-primary-400 transition-colors"
|
||||||
|
>
|
||||||
|
<User :size="16" />
|
||||||
|
<span>Mitglieder</span>
|
||||||
|
<ChevronUp :size="14" :class="['transition-transform', isMemberMenuOpen ? 'rotate-0' : 'rotate-180']" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Dropdown Menu (appears above) -->
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition duration-200 ease-out"
|
||||||
|
enter-from-class="transform translate-y-2 opacity-0"
|
||||||
|
enter-to-class="transform translate-y-0 opacity-100"
|
||||||
|
leave-active-class="transition duration-150 ease-in"
|
||||||
|
leave-from-class="transform translate-y-0 opacity-100"
|
||||||
|
leave-to-class="transform translate-y-2 opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isMemberMenuOpen"
|
||||||
|
class="absolute bottom-full right-0 mb-2 w-48 bg-gray-800 border border-gray-700 rounded-lg shadow-xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<template v-if="isLoggedIn">
|
||||||
|
<NuxtLink
|
||||||
|
to="/mitgliederbereich"
|
||||||
|
@click="isMemberMenuOpen = false"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Mitgliederbereich
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="isAdmin"
|
||||||
|
to="/cms"
|
||||||
|
@click="isMemberMenuOpen = false"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
CMS
|
||||||
|
</NuxtLink>
|
||||||
|
<button
|
||||||
|
@click="handleLogout"
|
||||||
|
class="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Abmelden
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<NuxtLink
|
||||||
|
to="/login"
|
||||||
|
@click="isMemberMenuOpen = false"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Anmelden
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,5 +79,57 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { User, ChevronUp } from 'lucide-vue-next'
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
|
const isMemberMenuOpen = ref(false)
|
||||||
|
const isLoggedIn = ref(false)
|
||||||
|
const userRole = ref(null)
|
||||||
|
|
||||||
|
const isAdmin = computed(() => {
|
||||||
|
return userRole.value === 'admin' || userRole.value === 'vorstand'
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleMemberMenu = () => {
|
||||||
|
isMemberMenuOpen.value = !isMemberMenuOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await $fetch('/api/auth/logout', { method: 'POST' })
|
||||||
|
isLoggedIn.value = false
|
||||||
|
userRole.value = null
|
||||||
|
isMemberMenuOpen.value = false
|
||||||
|
navigateTo('/')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout fehlgeschlagen:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check auth status on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/auth/status')
|
||||||
|
isLoggedIn.value = response.isLoggedIn
|
||||||
|
userRole.value = response.role
|
||||||
|
} catch (error) {
|
||||||
|
isLoggedIn.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close menu when clicking outside
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (!event.target.closest('.relative')) {
|
||||||
|
isMemberMenuOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
29
middleware/auth.js
Normal file
29
middleware/auth.js
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"nodemailer": "^7.0.9",
|
"nodemailer": "^7.0.9",
|
||||||
"nuxt": "^3.11.0",
|
"nuxt": "^3.11.0",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
|
|||||||
124
pages/cms/index.vue
Normal file
124
pages/cms/index.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-full py-16 bg-gray-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-6">
|
||||||
|
Content Management System
|
||||||
|
</h1>
|
||||||
|
<div class="w-24 h-1 bg-primary-600 mb-8" />
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-lg p-8 mb-8">
|
||||||
|
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">
|
||||||
|
Willkommen im CMS, {{ user?.name }}!
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Hier können Sie Inhalte der Website verwalten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CMS Modules -->
|
||||||
|
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mr-4">
|
||||||
|
<Calendar :size="24" class="text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">Termine verwalten</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 text-sm mb-4">
|
||||||
|
Termine hinzufügen, bearbeiten und löschen
|
||||||
|
</p>
|
||||||
|
<button class="text-sm text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Öffnen →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mr-4">
|
||||||
|
<Newspaper :size="24" class="text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">Interne News</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 text-sm mb-4">
|
||||||
|
News für Mitglieder erstellen und verwalten
|
||||||
|
</p>
|
||||||
|
<button class="text-sm text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Öffnen →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mr-4">
|
||||||
|
<FileText :size="24" class="text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">Spielpläne</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 text-sm mb-4">
|
||||||
|
Spielpläne hochladen und verwalten
|
||||||
|
</p>
|
||||||
|
<button class="text-sm text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Öffnen →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mr-4">
|
||||||
|
<Users :size="24" class="text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">Mitglieder</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 text-sm mb-4">
|
||||||
|
Mitgliederdaten verwalten
|
||||||
|
</p>
|
||||||
|
<button class="text-sm text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Öffnen →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mr-4">
|
||||||
|
<Image :size="24" class="text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">Galerie</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 text-sm mb-4">
|
||||||
|
Bilder hochladen und verwalten
|
||||||
|
</p>
|
||||||
|
<button class="text-sm text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Öffnen →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { Calendar, Newspaper, FileText, Users, Image } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const user = ref(null)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/auth/status')
|
||||||
|
if (response.isLoggedIn) {
|
||||||
|
user.value = response.user
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Benutzerdaten:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth'
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'CMS - Harheimer TC',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
152
pages/login.vue
Normal file
152
pages/login.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-full flex items-center justify-center py-16 px-4 sm:px-6 lg:px-8 bg-gray-50">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="text-3xl font-display font-bold text-gray-900">
|
||||||
|
Mitglieder-Login
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">
|
||||||
|
Melden Sie sich an, um auf den Mitgliederbereich zuzugreifen
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-lg p-8">
|
||||||
|
<form @submit.prevent="handleLogin" class="space-y-6">
|
||||||
|
<!-- Email -->
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
E-Mail-Adresse
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="formData.email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
autocomplete="email"
|
||||||
|
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"
|
||||||
|
:class="{ 'border-red-500': errorMessage }"
|
||||||
|
placeholder="ihre-email@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Passwort
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="formData.password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
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"
|
||||||
|
:class="{ 'border-red-500': errorMessage }"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div v-if="errorMessage" class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-red-800 flex items-center">
|
||||||
|
<AlertCircle :size="18" class="mr-2" />
|
||||||
|
{{ errorMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success Message -->
|
||||||
|
<div v-if="successMessage" class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-green-800 flex items-center">
|
||||||
|
<Check :size="18" class="mr-2" />
|
||||||
|
{{ successMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="w-full px-6 py-3 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="isLoading" :size="20" class="mr-2 animate-spin" />
|
||||||
|
<span>{{ isLoading ? 'Anmeldung läuft...' : 'Anmelden' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Forgot Password Link -->
|
||||||
|
<div class="text-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/passwort-vergessen"
|
||||||
|
class="text-sm text-primary-600 hover:text-primary-700 font-medium"
|
||||||
|
>
|
||||||
|
Passwort vergessen?
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Box -->
|
||||||
|
<div class="bg-primary-50 border border-primary-100 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-primary-800 text-center">
|
||||||
|
<Lock :size="16" class="inline mr-1" />
|
||||||
|
Nur für Vereinsmitglieder. Kein Zugang? Kontaktieren Sie den Vorstand.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { AlertCircle, Check, Loader2, Lock } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const successMessage = ref('')
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
successMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
email: formData.value.email,
|
||||||
|
password: formData.value.password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
successMessage.value = 'Anmeldung erfolgreich! Sie werden weitergeleitet...'
|
||||||
|
|
||||||
|
// Redirect based on role
|
||||||
|
setTimeout(() => {
|
||||||
|
if (response.user.role === 'admin' || response.user.role === 'vorstand') {
|
||||||
|
navigateTo('/cms')
|
||||||
|
} else {
|
||||||
|
navigateTo('/mitgliederbereich')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error.data?.message || 'Anmeldung fehlgeschlagen. Bitte prüfen Sie Ihre Zugangsdaten.'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'default'
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'Login - Harheimer TC',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
111
pages/mitgliederbereich/index.vue
Normal file
111
pages/mitgliederbereich/index.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-full py-16 bg-gray-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-6">
|
||||||
|
Mitgliederbereich
|
||||||
|
</h1>
|
||||||
|
<div class="w-24 h-1 bg-primary-600 mb-8" />
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-lg p-8 mb-8">
|
||||||
|
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">
|
||||||
|
Willkommen, {{ user?.name }}!
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
Sie sind als <span class="font-semibold text-primary-600">{{ roleLabel }}</span> angemeldet.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Letzter Login: {{ lastLoginFormatted }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Links -->
|
||||||
|
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<NuxtLink
|
||||||
|
to="/mitgliederbereich/mitglieder"
|
||||||
|
class="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow border border-gray-100"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mr-4">
|
||||||
|
<Users :size="24" class="text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">Mitgliederliste</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 text-sm">
|
||||||
|
Kontaktdaten aller Vereinsmitglieder
|
||||||
|
</p>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
to="/mitgliederbereich/news"
|
||||||
|
class="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow border border-gray-100"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mr-4">
|
||||||
|
<Newspaper :size="24" class="text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">Interne News</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 text-sm">
|
||||||
|
Neuigkeiten nur für Mitglieder
|
||||||
|
</p>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
to="/mitgliederbereich/profil"
|
||||||
|
class="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow border border-gray-100"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mr-4">
|
||||||
|
<UserCog :size="24" class="text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">Mein Profil</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 text-sm">
|
||||||
|
Profil bearbeiten und Passwort ändern
|
||||||
|
</p>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { Users, Newspaper, UserCog } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const user = ref(null)
|
||||||
|
|
||||||
|
const roleLabel = computed(() => {
|
||||||
|
const labels = {
|
||||||
|
admin: 'Administrator',
|
||||||
|
vorstand: 'Vorstand',
|
||||||
|
mitglied: 'Mitglied'
|
||||||
|
}
|
||||||
|
return labels[user.value?.role] || 'Mitglied'
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastLoginFormatted = computed(() => {
|
||||||
|
if (!user.value?.lastLogin) return 'Erste Anmeldung'
|
||||||
|
return new Date(user.value.lastLogin).toLocaleString('de-DE')
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/auth/status')
|
||||||
|
if (response.isLoggedIn) {
|
||||||
|
user.value = response.user
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Benutzerdaten:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth'
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'Mitgliederbereich - Harheimer TC',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
115
pages/passwort-vergessen.vue
Normal file
115
pages/passwort-vergessen.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-full flex items-center justify-center py-16 px-4 sm:px-6 lg:px-8 bg-gray-50">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="text-3xl font-display font-bold text-gray-900">
|
||||||
|
Passwort zurücksetzen
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">
|
||||||
|
Geben Sie Ihre E-Mail-Adresse ein, um Ihr Passwort zurückzusetzen
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-lg p-8">
|
||||||
|
<form @submit.prevent="handleReset" class="space-y-6">
|
||||||
|
<!-- Email -->
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
E-Mail-Adresse
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
autocomplete="email"
|
||||||
|
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"
|
||||||
|
:class="{ 'border-red-500': errorMessage }"
|
||||||
|
placeholder="ihre-email@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div v-if="errorMessage" class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-red-800 flex items-center">
|
||||||
|
<AlertCircle :size="18" class="mr-2" />
|
||||||
|
{{ errorMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success Message -->
|
||||||
|
<div v-if="successMessage" class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-green-800 flex items-center">
|
||||||
|
<Check :size="18" class="mr-2" />
|
||||||
|
{{ successMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="w-full px-6 py-3 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="isLoading" :size="20" class="mr-2 animate-spin" />
|
||||||
|
<span>{{ isLoading ? 'Wird gesendet...' : 'Passwort zurücksetzen' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Back to Login -->
|
||||||
|
<div class="text-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/login"
|
||||||
|
class="text-sm text-primary-600 hover:text-primary-700 font-medium"
|
||||||
|
>
|
||||||
|
Zurück zum Login
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Box -->
|
||||||
|
<div class="bg-primary-50 border border-primary-100 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-primary-800 text-center">
|
||||||
|
Sie erhalten eine E-Mail mit einem Link zum Zurücksetzen Ihres Passworts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { AlertCircle, Check, Loader2 } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const email = ref('')
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const successMessage = ref('')
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
successMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/auth/reset-password', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { email: email.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
successMessage.value = 'Eine E-Mail mit weiteren Anweisungen wurde an Ihre E-Mail-Adresse gesendet.'
|
||||||
|
email.value = ''
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error.data?.message || 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'Passwort vergessen - Harheimer TC',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
69
server/api/auth/login.post.js
Normal file
69
server/api/auth/login.post.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
26
server/api/auth/logout.post.js
Normal file
26
server/api/auth/logout.post.js
Normal file
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
82
server/api/auth/reset-password.post.js
Normal file
82
server/api/auth/reset-password.post.js
Normal file
@@ -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: `
|
||||||
|
<h2>Passwort zurücksetzen</h2>
|
||||||
|
<p>Hallo ${user.name},</p>
|
||||||
|
<p>Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt.</p>
|
||||||
|
<p>Ihr temporäres Passwort lautet: <strong>${tempPassword}</strong></p>
|
||||||
|
<p>Bitte melden Sie sich damit an und ändern Sie Ihr Passwort im Mitgliederbereich.</p>
|
||||||
|
<br>
|
||||||
|
<p>Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail.</p>
|
||||||
|
<br>
|
||||||
|
<p>Mit sportlichen Grüßen,<br>Ihr Harheimer TC</p>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
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.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
45
server/api/auth/status.get.js
Normal file
45
server/api/auth/status.get.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
12
server/data/members.json
Normal file
12
server/data/members.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Admin",
|
||||||
|
"email": "admin@harheimertc.de",
|
||||||
|
"phone": "069-12345678",
|
||||||
|
"role": "admin",
|
||||||
|
"memberSince": "2020-01-01",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
2
server/data/sessions.json
Normal file
2
server/data/sessions.json
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[]
|
||||||
|
|
||||||
13
server/data/users.json
Normal file
13
server/data/users.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
125
server/utils/auth.js
Normal file
125
server/utils/auth.js
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user