feat: add auth guards and server-side logout with postcode search integration
- Add navigation guard requiring authentication for dashboard routes - Create
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user