Sanctum's statefulApi() runs same-origin API requests through the web middleware stack, which enforces CSRF. Send X-CSRF-TOKEN from the meta tag and credentials: same-origin so cookies travel with the request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
112 lines
4.7 KiB
PHP
112 lines
4.7 KiB
PHP
@extends('layouts.web', ['title' => 'Dashboard — Verde'])
|
||
|
||
@section('content')
|
||
<div id="dashboard-root" class="min-h-screen">
|
||
{{-- Top nav --}}
|
||
<nav class="sticky top-0 z-10 border-b border-neutral-200 bg-white/80 backdrop-blur">
|
||
<div class="mx-auto flex h-14 max-w-7xl items-center justify-between px-6">
|
||
<div class="flex items-center gap-2.5">
|
||
<div class="flex h-7 w-7 items-center justify-center rounded bg-verde-600">
|
||
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4 text-white" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 2c-3 5-7 8-7 13a7 7 0 0 0 14 0c0-5-4-8-7-13z"/>
|
||
</svg>
|
||
</div>
|
||
<span class="text-sm font-semibold tracking-tight text-neutral-900">Verde</span>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-3">
|
||
<span id="user-name" class="text-sm text-neutral-600"></span>
|
||
<button id="logout-btn" class="btn-ghost px-3 py-1.5 text-xs">Sign out</button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main class="mx-auto max-w-7xl px-6 py-12">
|
||
<header class="mb-10">
|
||
<h1 class="text-2xl font-semibold tracking-tight text-neutral-900">
|
||
Welcome <span id="welcome-first" class="text-verde-700"></span>
|
||
</h1>
|
||
<p class="mt-1 text-sm text-neutral-500">Admin dashboard scaffolding — full UI coming soon.</p>
|
||
</header>
|
||
|
||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||
@foreach ([
|
||
['label' => 'Trips today', 'value' => '—'],
|
||
['label' => 'In progress', 'value' => '—'],
|
||
['label' => 'Verified households', 'value' => '—'],
|
||
['label' => 'Active QR codes', 'value' => '—'],
|
||
] as $stat)
|
||
<div class="rounded-lg border border-neutral-200 bg-white p-5">
|
||
<div class="text-xs font-medium uppercase tracking-wider text-neutral-500">{{ $stat['label'] }}</div>
|
||
<div class="mt-2 text-2xl font-semibold tracking-tight text-neutral-900">{{ $stat['value'] }}</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
|
||
<div class="mt-10 rounded-lg border border-neutral-200 bg-white p-6">
|
||
<h2 class="text-base font-semibold text-neutral-900">Backend status</h2>
|
||
<p class="mt-1 text-sm text-neutral-500">Modules complete: 1–8 (Auth, Geo, Users, Households, DOPs, Dumpsites, QR, Routes).</p>
|
||
<ul class="mt-4 space-y-1.5 text-sm text-neutral-700">
|
||
<li class="flex items-center gap-2">
|
||
<span class="h-1.5 w-1.5 rounded-full bg-verde-500"></span>
|
||
API: <code class="font-mono text-xs text-neutral-500">/api/v1/health</code>
|
||
</li>
|
||
<li class="flex items-center gap-2">
|
||
<span class="h-1.5 w-1.5 rounded-full bg-verde-500"></span>
|
||
Tests passing: <span class="font-mono text-xs">139</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script type="module">
|
||
const token = localStorage.getItem('verde:token');
|
||
if (!token) {
|
||
window.location.href = '/login';
|
||
}
|
||
|
||
const userNameEl = document.getElementById('user-name');
|
||
const welcomeFirstEl = document.getElementById('welcome-first');
|
||
|
||
fetch('/api/v1/auth/me', {
|
||
headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' },
|
||
})
|
||
.then(res => {
|
||
if (res.status === 401) {
|
||
localStorage.removeItem('verde:token');
|
||
window.location.href = '/login';
|
||
throw new Error('unauthenticated');
|
||
}
|
||
return res.json();
|
||
})
|
||
.then(body => {
|
||
const u = body.data?.user;
|
||
if (u) {
|
||
userNameEl.textContent = u.email;
|
||
welcomeFirstEl.textContent = u.first_name + '.';
|
||
}
|
||
})
|
||
.catch(() => {});
|
||
|
||
document.getElementById('logout-btn').addEventListener('click', async () => {
|
||
try {
|
||
const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
|
||
await fetch('/api/v1/auth/logout', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Accept': 'application/json',
|
||
'X-CSRF-TOKEN': csrf,
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
},
|
||
});
|
||
} catch {}
|
||
localStorage.removeItem('verde:token');
|
||
localStorage.removeItem('verde:user');
|
||
window.location.href = '/login';
|
||
});
|
||
</script>
|
||
@endsection
|