Files
adam-acs/inertia/layouts/MainLayout.vue
T
Kevin L'Hereec 991173aa05 feat(auth): add session login page, API key guard, and remove orphan SQL migrations
- Add session-based authentication for web-UI (Login.vue, AuthController)
- Add AuthSessionMiddleware: redirects unauthenticated users to /login
- Add ApiKeyMiddleware: validates X-Api-Key header on all /api/v1 routes
- Add BasicAuthMiddleware: registered for future M2M use
- Use timingSafeEqual for all credential comparisons (timing attack prevention)
- Regenerate session ID on login (session fixation prevention)
- Validate redirect target after login (open redirect prevention)
- Require WEBUI_USERNAME, WEBUI_PASSWORD, API_KEY as mandatory env vars
- Share auth.username/authenticated via Inertia shared props
- Add user dropdown in navbar (avatar icon + username + logout)
- Add login i18n keys in fr.json and en.json
- Remove orphan SQL migrations (users, auth_access_tokens) and @adonisjs/lucid
2026-07-03 13:59:05 +02:00

325 lines
9.3 KiB
Vue

<script setup lang="ts">
import { Link, Head, usePage, router } from '@inertiajs/vue3'
import { useI18n } from 'vue-i18n'
import { ref, computed } from 'vue'
const { t, locale } = useI18n()
const currentLocale = ref(locale.value)
const page = usePage()
const langMenuOpen = ref(false)
const userMenuOpen = ref(false)
function toggleUserMenu() {
userMenuOpen.value = !userMenuOpen.value
if (userMenuOpen.value) langMenuOpen.value = false
}
const auth = computed(() => (page.props as any).auth as { username: string | null; authenticated: boolean })
function switchLocale(lang: string) {
locale.value = lang
currentLocale.value = lang
langMenuOpen.value = false
}
function toggleLangMenu() {
langMenuOpen.value = !langMenuOpen.value
if (langMenuOpen.value) userMenuOpen.value = false
}
function logout() {
router.post('/logout')
}
function isActive(paths: string | string[]): boolean {
const url = page.url
if (Array.isArray(paths)) {
return paths.some((p) => url === p || (p !== '/' && url.startsWith(p)))
}
return url === paths || (paths !== '/' && url.startsWith(paths))
}
</script>
<template>
<Head :title="page.component" />
<div class="d-flex flex-column min-vh-100">
<!-- Navbar -->
<header>
<div class="bg-dark">
<div class="container-xxl">
<nav id="main-menu" class="navbar navbar-expand-lg navbar-dark bg-dark">
<!-- Brand / Logo -->
<h1 class="navbar-brand mb-0 me-3">
<Link href="/">
<span class="visually-hidden">{{ t('header.title') }}</span>
<svg
width="30"
height="30"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<rect width="100" height="100" fill="#ff7900" />
<rect x="14.29" y="71.43" width="71.43" height="14.29" fill="#fff" />
</svg>
</Link>
</h1>
<!-- Mobile toggle -->
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarContent"
aria-controls="navbarContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<!-- Nav links -->
<ul class="navbar-nav_container navbar-nav">
<li class="nav-item">
<Link
href="/"
class="nav-link"
:class="{
active:
isActive('/') &&
!page.url.startsWith('/configurations') &&
!page.url.startsWith('/commands'),
}"
>
{{ t('header.devices') }}
</Link>
</li>
<li class="nav-item">
<Link
href="/configurations"
class="nav-link"
:class="{ active: isActive('/configurations') }"
>
{{ t('header.configurations') }}
</Link>
</li>
<li class="nav-item">
<Link
href="/firmware-catalog"
class="nav-link"
:class="{ active: isActive('/firmware-catalog') }"
>
{{ t('header.firmware') }}
</Link>
</li>
<li class="nav-item">
<Link href="/commands" class="nav-link" :class="{ active: isActive('/commands') }">
{{ t('header.commands') }}
</Link>
</li>
<li class="nav-item">
<Link
href="/tr069-trace"
class="nav-link"
:class="{ active: isActive('/tr069-trace') }"
>
{{ t('header.tr069Trace') }}
</Link>
</li>
</ul>
<!-- Right: language dropdown + logout -->
<div class="navbar-right">
<ul class="navbar-nav navbar-nav_container">
<li class="nav-item dropdown" :class="{ show: langMenuOpen }">
<a
class="nav-link dropdown-toggle"
href="javascript:void(0);"
role="button"
@click="toggleLangMenu"
:aria-expanded="langMenuOpen"
>
<span>{{ currentLocale.toUpperCase() }}</span>
</a>
<ul class="dropdown-menu dropdown-menu-end" :class="{ show: langMenuOpen }">
<li>
<a
class="dropdown-item"
:class="{ active: currentLocale === 'fr' }"
href="javascript:void(0);"
@click="switchLocale('fr')"
>
FR — Français
</a>
</li>
<li>
<a
class="dropdown-item"
:class="{ active: currentLocale === 'en' }"
href="javascript:void(0);"
@click="switchLocale('en')"
>
EN — English
</a>
</li>
</ul>
</li>
<!-- User dropdown -->
<li v-if="auth.authenticated" class="nav-item dropdown" :class="{ show: userMenuOpen }">
<button
type="button"
class="nav-link user-menu-toggle"
@click="toggleUserMenu"
:aria-expanded="userMenuOpen"
>
<!-- Person icon -->
<div class="mb-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/>
</svg>
</div>
<span class="user-name">{{ auth.username }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" :class="{ show: userMenuOpen }">
<li>
<button type="button" class="dropdown-item" @click="logout">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" class="me-2">
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5-5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/>
</svg>
{{ t('login.logout') }}
</button>
</li>
</ul>
</li>
</ul>
</div>
</nav>
</div>
</div>
</header>
<!-- Main Content -->
<main class="flex-grow-1" style="background-color: #f6f6f6">
<slot />
</main>
<!-- Footer -->
<footer class="footer navbar fixed-bottom bg-dark" data-bs-theme="dark">
<h2 class="visually-hidden">Sitemap &amp; information</h2>
<div class="container-xxl footer-terms">
<ul class="navbar-nav flex-row gap-md-3">
<li class="fw-bold text-white">© Orange 2025</li>
<li>
<a class="nav-link" href="#">{{ t('footer.legal') }}</a>
</li>
<li>
<a class="nav-link" href="#">{{ t('footer.privacy') }}</a>
</li>
</ul>
</div>
</footer>
</div>
</template>
<style>
main {
padding-bottom: 80px;
}
#main-menu {
display: flex;
align-items: center;
}
#main-menu .navbar-brand {
margin: 0;
padding: 0.5rem;
}
#main-menu .navbar-brand a {
display: inline-block;
padding-bottom: 0.25rem;
}
#main-menu .navbar-brand svg {
display: block;
}
#main-menu .nav-link.active {
padding-bottom: 0.75rem;
}
.navbar-nav_container {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 0.25rem;
flex-grow: 1;
}
.navbar-right {
margin-left: auto;
display: flex;
align-items: center;
}
.navbar-right .navbar-nav {
display: flex;
flex-direction: row;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
}
.navbar-right .nav-link {
display: inline-flex;
align-items: center;
}
.nav-item.dropdown .dropdown-menu {
position: absolute;
top: 100%;
right: 0;
left: auto;
}
.dropdown-item.active {
background-color: #ff7900;
color: #fff;
}
.user-menu-toggle {
background: none;
border: none;
padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);
color: rgba(255, 255, 255, 0.55);
display: inline-flex;
align-items: center;
gap: 0.4rem;
cursor: pointer;
font-size: 1rem;
line-height: 1;
}
.user-menu-toggle svg {
display: block;
flex-shrink: 0;
}
.user-menu-toggle:hover,
.user-menu-toggle:focus {
color: rgba(255, 255, 255, 0.75);
outline: none;
}
.user-name {
line-height: 1;
}
</style>