Files
oauth/resources/js/App.vue
2026-07-27 17:20:02 +02:00

111 lines
5.0 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useAuthStore } from './stores/auth';
import { useTheme } from './composables/useTheme';
import AppLogo from './components/AppLogo.vue';
const auth = useAuthStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { isDark, toggleTheme } = useTheme();
const showNav = computed(() => auth.isAuthenticated && route.name !== 'oauth-authorize');
const currentLocale = window.__SESSION_LOCALE__ ?? 'es';
async function logout(): Promise<void> {
await auth.logout();
router.push({ name: 'login' });
}
async function setLocale(locale: string): Promise<void> {
await axios.post('/web-api/locale', { locale });
window.location.reload();
}
function onLocaleChange(event: Event): void {
setLocale((event.target as HTMLSelectElement).value);
}
interface NavLink {
to: string;
name: string;
label: string;
}
const navLinks = computed<NavLink[]>(() => [
{ to: '/', name: 'dashboard', label: t('nav.dashboard') },
{ to: '/clients', name: 'clients', label: t('nav.clients') },
{ to: '/tokens', name: 'tokens', label: t('nav.tokens') },
{ to: '/authorized-apps', name: 'authorized-apps', label: t('nav.authorizedApps') },
{ to: '/security', name: 'security', label: t('nav.security') },
...(auth.isAdmin ? [{ to: '/admin/users', name: 'admin-users', label: t('nav.users') }] : []),
...(auth.isAdmin ? [{ to: '/admin/analytics', name: 'admin-analytics', label: t('nav.analytics') }] : []),
...(auth.isAdmin ? [{ to: '/admin/audit-log', name: 'admin-audit-log', label: t('nav.auditLog') }] : []),
]);
</script>
<template>
<div class="app-shell">
<nav v-if="showNav" class="app-nav no-print">
<div class="app-nav__inner">
<div class="app-nav__top">
<router-link to="/" class="app-nav__brand">
<AppLogo class="app-nav__logo" />
<span class="app-nav__brand-name">OAuth2 Server</span>
</router-link>
<div class="app-nav__controls">
<select :value="currentLocale" @change="onLocaleChange" class="pill-select">
<option value="es">ES</option>
<option value="en">EN</option>
<option value="ca">CA</option>
</select>
<button @click="toggleTheme" type="button" class="app-nav__theme-toggle"
:aria-label="isDark ? 'Light mode' : 'Dark mode'">
<svg v-if="isDark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M12 2.25a.75.75 0 01.75.75v2a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.415 1.414a.75.75 0 101.06 1.06l1.415-1.414zM21.75 12a.75.75 0 01-.75.75h-2a.75.75 0 010-1.5h2a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.414-1.415a.75.75 0 10-1.06 1.06l1.414 1.415zM12 18a.75.75 0 01.75.75v2a.75.75 0 01-1.5 0v-2A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.414 1.414a.75.75 0 001.06 1.06l1.415-1.414zM6 12a.75.75 0 01-.75.75h-2a.75.75 0 010-1.5h2A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06L6.343 5.283a.75.75 0 10-1.06 1.06l1.414 1.414z" clip-rule="evenodd" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.7-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z" clip-rule="evenodd" />
</svg>
</button>
<span class="app-nav__user">{{ auth.user?.email }}</span>
<button @click="logout" class="btn btn--invert">
{{ t('nav.logout') }}
</button>
</div>
</div>
<div class="app-nav__links">
<router-link
v-for="link in navLinks"
:key="link.name"
:to="link.to"
class="app-nav__link"
active-class="app-nav__link--active"
>
{{ link.label }}
</router-link>
<a href="/api/documentation" class="app-nav__link">{{ t('nav.apiDocs') }}</a>
</div>
</div>
</nav>
<main :class="showNav ? 'page' : ''">
<router-view />
</main>
</div>
</template>
<style scoped>
.app-shell {
min-height: 100vh;
}
</style>