Add authentication check and logout functionality in app.vue

This commit is contained in:
Jennie Robinson Faber 2025-08-23 12:47:40 +01:00
parent ee00a8018e
commit 733a1e9f47
9 changed files with 1294 additions and 1653 deletions

1
.gitignore vendored
View file

@ -23,3 +23,4 @@ logs
.env.*
!.env.example
.aider*
migrate-to-atlas.js

View file

@ -9,6 +9,15 @@
FABER FINANCES
</NuxtLink>
</div>
<div class="flex items-center">
<button
v-if="authenticated"
@click="logout"
class="text-white hover:text-gray-300 px-3 py-2 text-sm font-medium"
>
Logout
</button>
</div>
</div>
</div>
</nav>
@ -23,4 +32,27 @@ useSeoMeta({
titleTemplate: "%s - Faber Finances",
description: "Personal finance and cash flow management system",
});
const authenticated = ref(false)
// Check authentication status
onMounted(async () => {
try {
const { authenticated: isAuth } = await $fetch('/api/auth/check')
authenticated.value = isAuth
} catch (err) {
authenticated.value = false
}
})
// Logout function
const logout = async () => {
try {
await $fetch('/api/auth/logout', { method: 'POST' })
authenticated.value = false
await navigateTo('/login')
} catch (err) {
console.error('Logout error:', err)
}
}
</script>

View file

@ -12,6 +12,7 @@
"dependencies": {
"mongodb": "^6.18.0",
"nuxt": "^4.0.3",
"unstorage": "^1.17.0",
"vue": "^3.5.18",
"vue-router": "^4.5.1"
},

95
pages/login.vue Normal file
View file

@ -0,0 +1,95 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Faber Finances
</h2>
<p class="mt-2 text-center text-sm text-gray-600">
Enter the shared password to continue
</p>
</div>
<form class="mt-8 space-y-6" @submit.prevent="login">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="password" class="sr-only">Password</label>
<input
id="password"
v-model="password"
name="password"
type="password"
required
class="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
:disabled="loading"
/>
</div>
</div>
<div v-if="error" class="text-red-600 text-sm text-center">
{{ error }}
</div>
<div>
<button
type="submit"
:disabled="loading"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span v-if="loading">Signing in...</span>
<span v-else>Sign in</span>
</button>
</div>
</form>
</div>
</div>
</template>
<script setup>
definePageMeta({
layout: false
})
const password = ref('')
const loading = ref(false)
const error = ref('')
const login = async () => {
if (!password.value) {
error.value = 'Password is required'
return
}
loading.value = true
error.value = ''
try {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: {
password: password.value
}
})
if (response.success) {
await navigateTo('/')
}
} catch (err) {
error.value = err.data?.message || 'Invalid password'
} finally {
loading.value = false
}
}
// Check if already authenticated
onMounted(async () => {
try {
const { authenticated } = await $fetch('/api/auth/check')
if (authenticated) {
await navigateTo('/')
}
} catch (err) {
// Not authenticated, stay on login page
}
})
</script>

View file

@ -0,0 +1,21 @@
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth-token')
if (!token) {
return { authenticated: false }
}
const session = await useStorage('memory').getItem(`session:${token}`)
if (!session) {
return { authenticated: false }
}
// Check if session has expired
if (session.expiresAt && new Date() > new Date(session.expiresAt)) {
await useStorage('memory').removeItem(`session:${token}`)
return { authenticated: false }
}
return { authenticated: true }
})

View file

@ -0,0 +1,38 @@
import crypto from 'crypto'
export default defineEventHandler(async (event) => {
const { password } = await readBody(event)
if (!password) {
throw createError({
statusCode: 400,
statusMessage: 'Password is required'
})
}
const correctPassword = process.env.APP_PASSWORD
if (password !== correctPassword) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid password'
})
}
const sessionToken = crypto.randomBytes(32).toString('hex')
setCookie(event, 'auth-token', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7 days
})
await useStorage('memory').setItem(`session:${sessionToken}`, {
authenticated: true,
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + (60 * 60 * 24 * 7 * 1000)).toISOString() // 7 days
})
return { success: true }
})

View file

@ -0,0 +1,11 @@
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth-token')
if (token) {
await useStorage('memory').removeItem(`session:${token}`)
}
deleteCookie(event, 'auth-token')
return { success: true }
})

43
server/middleware/auth.js Normal file
View file

@ -0,0 +1,43 @@
export default defineEventHandler(async (event) => {
// Skip auth check for login page and auth API routes
if (event.node.req.url?.startsWith('/api/auth/') ||
event.node.req.url === '/login' ||
event.node.req.url?.startsWith('/_nuxt/') ||
event.node.req.url?.startsWith('/__nuxt_devtools__/')) {
return
}
// Only check auth for API routes and page requests
if (event.node.req.url?.startsWith('/api/') ||
!event.node.req.url?.includes('.')) {
const token = getCookie(event, 'auth-token')
if (!token) {
if (event.node.req.url?.startsWith('/api/')) {
throw createError({
statusCode: 401,
statusMessage: 'Authentication required'
})
}
// Redirect to login for page requests
return sendRedirect(event, '/login')
}
const session = await useStorage('memory').getItem(`session:${token}`)
if (!session || (session.expiresAt && new Date() > new Date(session.expiresAt))) {
if (session && session.expiresAt && new Date() > new Date(session.expiresAt)) {
await useStorage('memory').removeItem(`session:${token}`)
}
deleteCookie(event, 'auth-token')
if (event.node.req.url?.startsWith('/api/')) {
throw createError({
statusCode: 401,
statusMessage: 'Session expired'
})
}
return sendRedirect(event, '/login')
}
}
})

2705
yarn.lock

File diff suppressed because it is too large Load diff