111 lines
5.0 KiB
Vue
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>
|