Replace composable with Pinia store for persistent auth state

This commit is contained in:
Torsten Schulz (local)
2025-10-21 14:19:30 +02:00
parent 1015d37eb7
commit 43071b45a9
10 changed files with 137 additions and 69 deletions

View File

@@ -93,30 +93,32 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { User, ChevronUp } from 'lucide-vue-next' import { User, ChevronUp } from 'lucide-vue-next'
const router = useRouter() const router = useRouter()
const authStore = useAuthStore()
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
const isMemberMenuOpen = ref(false) const isMemberMenuOpen = ref(false)
// Use global auth state // Reactive auth state from store
const { isLoggedIn, isAdmin, logout: authLogout, checkAuth } = useAuth() const isLoggedIn = computed(() => authStore.isLoggedIn)
const isAdmin = computed(() => authStore.isAdmin)
const toggleMemberMenu = () => { const toggleMemberMenu = () => {
isMemberMenuOpen.value = !isMemberMenuOpen.value isMemberMenuOpen.value = !isMemberMenuOpen.value
} }
const handleLogout = async () => { const handleLogout = async () => {
await authLogout() await authStore.logout()
isMemberMenuOpen.value = false isMemberMenuOpen.value = false
router.push('/') router.push('/')
} }
// Check auth status on mount // Check auth status on mount
onMounted(() => { onMounted(() => {
checkAuth() authStore.checkAuth()
}) })
// Close menu when clicking outside // Close menu when clicking outside

View File

@@ -396,13 +396,15 @@ import { useRoute } from 'vue-router'
import { Menu, X, ChevronDown } from 'lucide-vue-next' import { Menu, X, ChevronDown } from 'lucide-vue-next'
const route = useRoute() const route = useRoute()
const authStore = useAuthStore()
const isMobileMenuOpen = ref(false) const isMobileMenuOpen = ref(false)
const mobileSubmenu = ref(null) const mobileSubmenu = ref(null)
const mannschaften = ref([]) const mannschaften = ref([])
const hasGalleryImages = ref(false) const hasGalleryImages = ref(false)
// Use global auth state // Reactive auth state from store
const { isLoggedIn, userRole, isAdmin, checkAuth } = useAuth() const isLoggedIn = computed(() => authStore.isLoggedIn)
const isAdmin = computed(() => authStore.isAdmin)
// Automatisches Setzen des Submenus basierend auf der Route // Automatisches Setzen des Submenus basierend auf der Route
const currentSubmenu = computed(() => { const currentSubmenu = computed(() => {
@@ -486,7 +488,7 @@ const checkGalleryImages = async () => {
onMounted(() => { onMounted(() => {
loadMannschaften() loadMannschaften()
checkGalleryImages() checkGalleryImages()
checkAuth() authStore.checkAuth()
}) })
const toggleSubmenu = (menu) => { const toggleSubmenu = (menu) => {

View File

@@ -1,48 +0,0 @@
import { ref, computed } from 'vue'
// Global state (shared across all components)
const isLoggedIn = ref(false)
const user = ref(null)
const userRole = ref(null)
export const useAuth = () => {
const isAdmin = computed(() => {
return userRole.value === 'admin' || userRole.value === 'vorstand'
})
const checkAuth = async () => {
try {
const response = await $fetch('/api/auth/status')
isLoggedIn.value = response.isLoggedIn
user.value = response.user
userRole.value = response.role
return response
} catch (error) {
isLoggedIn.value = false
user.value = null
userRole.value = null
return { isLoggedIn: false }
}
}
const logout = async () => {
try {
await $fetch('/api/auth/logout', { method: 'POST' })
isLoggedIn.value = false
user.value = null
userRole.value = null
} catch (error) {
console.error('Logout fehlgeschlagen:', error)
}
}
return {
isLoggedIn,
user,
userRole,
isAdmin,
checkAuth,
logout
}
}

View File

@@ -2,7 +2,7 @@
export default defineNuxtConfig({ export default defineNuxtConfig({
devtools: { enabled: true }, devtools: { enabled: true },
modules: ['@nuxtjs/tailwindcss'], modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
nitro: { nitro: {
preset: 'node-server', preset: 'node-server',

47
package-lock.json generated
View File

@@ -9,10 +9,12 @@
"version": "1.0.0", "version": "1.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.11.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"nodemailer": "^7.0.9", "nodemailer": "^7.0.9",
"nuxt": "^4.1.3", "nuxt": "^4.1.3",
"pinia": "^3.0.3",
"vue": "^3.5.22" "vue": "^3.5.22"
}, },
"devDependencies": { "devDependencies": {
@@ -2530,6 +2532,21 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/@pinia/nuxt": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.11.2.tgz",
"integrity": "sha512-CgvSWpbktxxWBV7ModhAcsExsQZqpPq6vMYEe9DexmmY6959ev8ukL4iFhr/qov2Nb9cQAWd7niFDnaWkN+FHg==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.9.0"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"pinia": "^3.0.3"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -8123,6 +8140,36 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.7"
}
},
"node_modules/pirates": { "node_modules/pirates": {
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",

View File

@@ -13,10 +13,12 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.11.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"nodemailer": "^7.0.9", "nodemailer": "^7.0.9",
"nuxt": "^4.1.3", "nuxt": "^4.1.3",
"pinia": "^3.0.3",
"vue": "^3.5.22" "vue": "^3.5.22"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -101,7 +101,7 @@ import { useRouter } from 'vue-router'
import { AlertCircle, Check, Loader2, Lock } from 'lucide-vue-next' import { AlertCircle, Check, Loader2, Lock } from 'lucide-vue-next'
const router = useRouter() const router = useRouter()
const { checkAuth } = useAuth() const authStore = useAuthStore()
const formData = ref({ const formData = ref({
email: '', email: '',
@@ -118,20 +118,11 @@ const handleLogin = async () => {
successMessage.value = '' successMessage.value = ''
try { try {
const response = await $fetch('/api/auth/login', { const response = await authStore.login(formData.value.email, formData.value.password)
method: 'POST',
body: {
email: formData.value.email,
password: formData.value.password
}
})
if (response.success) { if (response.success) {
successMessage.value = 'Anmeldung erfolgreich! Sie werden weitergeleitet...' successMessage.value = 'Anmeldung erfolgreich! Sie werden weitergeleitet...'
// Update global auth state
await checkAuth()
// Redirect based on role // Redirect based on role
setTimeout(() => { setTimeout(() => {
if (response.user.role === 'admin' || response.user.role === 'vorstand') { if (response.user.role === 'admin' || response.user.role === 'vorstand') {

View File

@@ -40,5 +40,19 @@
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYxMDQ4NTgwLCJleHAiOjE3NjE2NTMzODB9._ZEOavin4scWHMI9ofk3MBNdx98K7Q_JossQYxUX4rQ", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYxMDQ4NTgwLCJleHAiOjE3NjE2NTMzODB9._ZEOavin4scWHMI9ofk3MBNdx98K7Q_JossQYxUX4rQ",
"createdAt": "2025-10-21T12:09:40.103Z", "createdAt": "2025-10-21T12:09:40.103Z",
"expiresAt": "2025-10-28T12:09:40.103Z" "expiresAt": "2025-10-28T12:09:40.103Z"
},
{
"id": "1761049028598",
"userId": "1",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYxMDQ5MDI4LCJleHAiOjE3NjE2NTM4Mjh9.FsC7CJpxVYVJ824vEPcZol4lKsCKU2I7-L644yeZuMw",
"createdAt": "2025-10-21T12:17:08.598Z",
"expiresAt": "2025-10-28T12:17:08.598Z"
},
{
"id": "1761049044973",
"userId": "1",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYxMDQ5MDQ0LCJleHAiOjE3NjE2NTM4NDR9.fhaWO80YN8qKy9zBCK5nauGWBT29hmr3KxAK_Vlab4E",
"createdAt": "2025-10-21T12:17:24.973Z",
"expiresAt": "2025-10-28T12:17:24.973Z"
} }
] ]

View File

@@ -8,6 +8,6 @@
"phone": "", "phone": "",
"active": true, "active": true,
"created": "2025-10-21T00:00:00.000Z", "created": "2025-10-21T00:00:00.000Z",
"lastLogin": "2025-10-21T12:09:40.103Z" "lastLogin": "2025-10-21T12:17:24.973Z"
} }
] ]

58
stores/auth.js Normal file
View File

@@ -0,0 +1,58 @@
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
isLoggedIn: false,
user: null,
role: null
}),
getters: {
isAdmin: (state) => {
return state.role === 'admin' || state.role === 'vorstand'
}
},
actions: {
async checkAuth() {
try {
const response = await $fetch('/api/auth/status')
this.isLoggedIn = response.isLoggedIn
this.user = response.user
this.role = response.role
return response
} catch (error) {
this.isLoggedIn = false
this.user = null
this.role = null
return { isLoggedIn: false }
}
},
async login(email, password) {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
if (response.success) {
await this.checkAuth()
}
return response
},
async logout() {
try {
await $fetch('/api/auth/logout', { method: 'POST' })
this.isLoggedIn = false
this.user = null
this.role = null
} catch (error) {
console.error('Logout fehlgeschlagen:', error)
throw error
}
}
}
})