122 lines
2.8 KiB
Vue
122 lines
2.8 KiB
Vue
<!--
|
|
The shell layout for all user-authenticated parts of the app. It defines a top
|
|
bar with some controls and user information, and a router view for all child
|
|
pages.
|
|
-->
|
|
<script setup lang="ts">
|
|
import { AuthApiClient } from '@/api/auth';
|
|
import { secondsUntilExpired } from '@/api/token-util';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { onMounted, ref, type Ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
|
|
const authCheckTimer: Ref<number | undefined> = ref(undefined)
|
|
|
|
onMounted(async () => {
|
|
authStore.$subscribe(async (_, state) => {
|
|
if (state.state === null) {
|
|
await router.replace('/login')
|
|
}
|
|
})
|
|
authCheckTimer.value = setInterval(checkAuth, 1000)
|
|
})
|
|
|
|
async function checkAuth() {
|
|
if (!authStore.state) {
|
|
clearInterval(authCheckTimer.value)
|
|
authCheckTimer.value = undefined
|
|
await router.replace('/login')
|
|
return
|
|
}
|
|
|
|
const secondsUntilExpiration = secondsUntilExpired(authStore.state.token)
|
|
if (secondsUntilExpiration < 60 && secondsUntilExpiration > 5) {
|
|
const api = new AuthApiClient()
|
|
try {
|
|
const newToken = await api.getNewToken()
|
|
authStore.state.token = newToken
|
|
} catch (err) {
|
|
console.warn('Failed to refresh token.', err)
|
|
}
|
|
} else if (secondsUntilExpiration <= 0) {
|
|
authStore.onUserLoggedOut()
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<header class="app-header-bar">
|
|
<div>
|
|
<h1 class="app-header-text" @click="router.push('/')">Finnow</h1>
|
|
</div>
|
|
<div>
|
|
<span class="app-user-widget">
|
|
Welcome, <em>{{ authStore.state?.username }}</em>
|
|
</span>
|
|
|
|
<span class="app-logout-button" @click="authStore.onUserLoggedOut()">
|
|
Log out
|
|
<font-awesome-icon icon="fa-solid fa-arrow-right-from-bracket"></font-awesome-icon>
|
|
</span>
|
|
</div>
|
|
</header>
|
|
|
|
<RouterView :key="$route.fullPath"></RouterView>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="css">
|
|
.app-header-bar {
|
|
background-color: var(--theme-primary);
|
|
color: var(--bg-primary);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.app-header-text {
|
|
display: inline;
|
|
margin: 0 1em 0 1em;
|
|
padding: 0;
|
|
font-family: 'PlaywriteNL';
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.app-header-text:hover {
|
|
color: var(--theme-secondary);
|
|
}
|
|
|
|
.app-logout-button {
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
margin-right: 1em;
|
|
}
|
|
|
|
.app-logout-button:hover {
|
|
color: var(--bg-secondary);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.app-user-widget {
|
|
margin-right: 1em;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.app-header-link {
|
|
font-weight: bold;
|
|
color: var(--bg-primary);
|
|
text-decoration: none;
|
|
margin: 0 0.5em 0 0.5em;
|
|
}
|
|
|
|
.app-header-link:hover {
|
|
color: var(--bg-secondary);
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|