feat: add auth guards and server-side logout with postcode search integration
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

- Add navigation guard requiring authentication for dashboard routes
- Create
This commit is contained in:
Ovidiu U
2026-04-11 17:08:19 +01:00
parent 4a3ce4cc1d
commit 03b0bece2c
4 changed files with 55 additions and 28 deletions

View File

@@ -8,12 +8,23 @@ import SettingsLayout from '../views/dashboard/settings/SettingsLayout.vue'
import Profile from '../views/dashboard/settings/Profile.vue' import Profile from '../views/dashboard/settings/Profile.vue'
import Security from '../views/dashboard/settings/Security.vue' import Security from '../views/dashboard/settings/Security.vue'
import Appearance from '../views/dashboard/settings/Appearance.vue' import Appearance from '../views/dashboard/settings/Appearance.vue'
import { useAuth } from '../composables/useAuth.js'
const routes = [ const routes = [
{ path: '/', component: Home, name: 'home' }, { path: '/', component: Home, name: 'home' },
{
path: '/logout',
name: 'logout',
component: { render: () => null },
beforeEnter: () => {
window.location.href = '/logout'
return false
},
},
{ {
path: '/dashboard', path: '/dashboard',
component: DashboardLayout, component: DashboardLayout,
meta: { requiresAuth: true },
children: [ children: [
{ path: '', component: Overview, name: 'dashboard' }, { path: '', component: Overview, name: 'dashboard' },
{ path: 'saved-stations', component: SavedStations, name: 'dashboard.saved-stations' }, { path: 'saved-stations', component: SavedStations, name: 'dashboard.saved-stations' },
@@ -32,7 +43,20 @@ const routes = [
}, },
] ]
export default createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes, routes,
}) })
router.beforeEach(async (to) => {
if (to.meta.requiresAuth) {
const { isAuthenticated, fetchUser } = useAuth()
await fetchUser()
if (!isAuthenticated.value) {
window.location.href = '/login'
return false
}
}
})
export default router

View File

@@ -30,7 +30,7 @@
</nav> </nav>
<!-- Hero --> <!-- Hero -->
<section class="relative pt-40 pb-24 px-6 hero-gradient overflow-hidden"> <section id="hero" class="relative pt-12 md:pt-40 pb-12 md:pb-24 px-6 hero-gradient overflow-hidden">
<div class="max-w-7xl mx-auto grid lg:grid-cols-2 gap-16 items-center"> <div class="max-w-7xl mx-auto grid lg:grid-cols-2 gap-16 items-center">
<div class="space-y-8"> <div class="space-y-8">
<div class="inline-flex items-center gap-2 px-3 py-1 bg-accent/10 text-accent rounded-full text-xs font-bold uppercase tracking-wider"> <div class="inline-flex items-center gap-2 px-3 py-1 bg-accent/10 text-accent rounded-full text-xs font-bold uppercase tracking-wider">
@@ -44,19 +44,7 @@
Join 50,000+ UK drivers using real-time insights to find the cheapest petrol and time their fill-ups perfectly. Join 50,000+ UK drivers using real-time insights to find the cheapest petrol and time their fill-ups perfectly.
</p> </p>
<div class="flex flex-col sm:flex-row gap-3 max-w-md"> <SearchBar @search="onSearch" />
<div class="relative flex-1">
<iconify-icon class="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 text-xl" icon="lucide:map-pin"></iconify-icon>
<input
class="w-full h-14 pl-12 pr-4 bg-white border border-zinc-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent shadow-inner text-lg"
placeholder="Enter Postcode"
type="text"
>
</div>
<button class="h-14 px-8 bg-accent text-white rounded-xl font-bold text-lg shadow-xl hover:bg-primary-dark transition-all">
Find Prices
</button>
</div>
<div class="flex items-center gap-4 pt-4"> <div class="flex items-center gap-4 pt-4">
<div class="flex -space-x-2"> <div class="flex -space-x-2">
@@ -69,7 +57,7 @@
</div> </div>
<!-- Visual mockup card --> <!-- Visual mockup card -->
<div class="relative"> <div class="relative hidden md:block">
<div class="absolute -inset-4 bg-accent/5 rounded-[2.5rem] blur-2xl"></div> <div class="absolute -inset-4 bg-accent/5 rounded-[2.5rem] blur-2xl"></div>
<div class="relative glass-card p-6 rounded-[2rem] shadow-2xl space-y-4 max-w-md mx-auto transform rotate-2"> <div class="relative glass-card p-6 rounded-[2rem] shadow-2xl space-y-4 max-w-md mx-auto transform rotate-2">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
@@ -106,7 +94,7 @@
</section> </section>
<!-- How It Works --> <!-- How It Works -->
<section id="how-it-works" class="py-24 px-6 bg-zinc-50"> <section id="how-it-works" class="py-12 md:py-24 px-6 bg-zinc-50">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-16 space-y-4"> <div class="text-center mb-16 space-y-4">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800">Smart Savings in 3 Steps</h2> <h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800">Smart Savings in 3 Steps</h2>
@@ -140,11 +128,11 @@
</section> </section>
<!-- Features --> <!-- Features -->
<section id="features" class="py-24 px-6"> <section id="features" class="py-12 md:py-24 px-6">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="grid lg:grid-cols-2 gap-20 items-center"> <div class="grid lg:grid-cols-2 gap-20 items-center">
<div class="order-2 lg:order-1"> <div class="order-2 lg:order-1">
<div class="grid grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl space-y-3"> <div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-accent" icon="lucide:zap"></iconify-icon> <iconify-icon class="text-3xl text-accent" icon="lucide:zap"></iconify-icon>
<h4 class="font-bold text-lg font-display">Real-Time Prices</h4> <h4 class="font-bold text-lg font-display">Real-Time Prices</h4>
@@ -195,7 +183,7 @@
</section> </section>
<!-- Pricing --> <!-- Pricing -->
<section id="pricing" class="py-24 px-6 bg-zinc-50"> <section id="pricing" class="py-12 md:py-24 px-6 bg-zinc-50">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-16"> <div class="text-center mb-16">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800 mb-4">Pricing for every driver</h2> <h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800 mb-4">Pricing for every driver</h2>
@@ -276,7 +264,7 @@
</section> </section>
<!-- Testimonials --> <!-- Testimonials -->
<section class="py-24 px-6"> <section class="py-12 md:py-24 px-6">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="flex flex-col md:flex-row gap-12 items-center"> <div class="flex flex-col md:flex-row gap-12 items-center">
<div class="md:w-1/3"> <div class="md:w-1/3">
@@ -317,7 +305,7 @@
</section> </section>
<!-- CTA --> <!-- CTA -->
<section class="py-24 px-6 bg-accent text-white text-center"> <section class="py-12 md:py-24 px-6 bg-accent text-white text-center">
<div class="max-w-3xl mx-auto space-y-8"> <div class="max-w-3xl mx-auto space-y-8">
<h2 class="text-4xl md:text-5xl font-black font-display leading-tight">Ready to outsmart the pumps?</h2> <h2 class="text-4xl md:text-5xl font-black font-display leading-tight">Ready to outsmart the pumps?</h2>
<p class="text-xl text-white/80">Sign up for free today and never pay over the odds for fuel again.</p> <p class="text-xl text-white/80">Sign up for free today and never pay over the odds for fuel again.</p>
@@ -388,8 +376,14 @@
</template> </template>
<script setup> <script setup>
import { RouterLink } from 'vue-router' import { RouterLink, useRouter } from 'vue-router'
import { useAuth } from '../composables/useAuth.js' import { useAuth } from '../composables/useAuth.js'
import SearchBar from '../components/SearchBar.vue'
const { isAuthenticated } = useAuth() const { isAuthenticated } = useAuth()
const router = useRouter()
function onSearch(postcode) {
router.push({ path: '/dashboard', query: { postcode } })
}
</script> </script>

View File

@@ -96,7 +96,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router' import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router'
import { useAuth } from '../../composables/useAuth.js' import { useAuth } from '../../composables/useAuth.js'
const { user, logout } = useAuth() const { user } = useAuth()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -129,10 +129,8 @@ const userInitials = computed(() => {
.toUpperCase() .toUpperCase()
}) })
async function handleLogout() { function handleLogout() {
dropdownOpen.value = false window.location.href = '/logout'
await logout()
router.push('/')
} }
function isActive(to) { function isActive(to) {

View File

@@ -1,9 +1,20 @@
<?php <?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
// Named dashboard route so route('dashboard') resolves; Vue Router handles rendering // Named dashboard route so route('dashboard') resolves; Vue Router handles rendering
Route::get('/dashboard', fn () => view('app'))->middleware(['auth', 'verified'])->name('dashboard'); Route::get('/dashboard', fn () => view('app'))->middleware(['auth', 'verified'])->name('dashboard');
// Server-side logout — handles hard navigation to /logout
Route::get('/logout', function (Request $request) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
})->middleware('auth')->name('logout');
// SPA catch-all — must be last // SPA catch-all — must be last
Route::get('/{any?}', fn () => view('app'))->where('any', '.*')->name('home'); Route::get('/{any?}', fn () => view('app'))->where('any', '.*')->name('home');