a bunch of changes
This commit is contained in:
parent
bdd0990922
commit
4bd99fd3e5
5 changed files with 117 additions and 17 deletions
24
app/app.vue
24
app/app.vue
|
|
@ -1,13 +1,28 @@
|
|||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
useHead({
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: '/css/main.css' }
|
||||
]
|
||||
})
|
||||
|
||||
const { isInitialized, verifySession } = useAuth()
|
||||
|
||||
onMounted(async () => {
|
||||
if (!isInitialized.value) {
|
||||
await verifySession()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<div class="app-viewport">
|
||||
<div v-if="!isInitialized" class="loading-container">
|
||||
<span class="spinner sm"></span>
|
||||
</div>
|
||||
|
||||
<div v-else class="app-viewport">
|
||||
<main class="app-content">
|
||||
<NuxtPage />
|
||||
</main>
|
||||
|
|
@ -17,6 +32,13 @@ useHead({
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-viewport {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -19,9 +19,18 @@ const emit = defineEmits(['logout', 'navigate'])
|
|||
MOD PORTAL
|
||||
</div>
|
||||
|
||||
<div v-if="userProfile" class="user-badge">
|
||||
<span class="user-name">{{ userProfile.username || userProfile.firstName }}</span>
|
||||
<span class="user-email">{{ userProfile.email }}</span>
|
||||
<div v-if="userProfile" class="user-badge" style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<img :src="userProfile.pid ? `https://mii.spfn.net/${userProfile.pid}/main.png` : '/img_unknown_MiiIcon.png'"
|
||||
:alt="userProfile.sfid || 'Mii Avatar'"
|
||||
style="width: 40px; height: 40px; border-radius: 50%; background: #161b22; object-fit: contain; flex-shrink: 0;"
|
||||
@error="(e) => e.target.src = '/img_unknown_MiiIcon.png'" />
|
||||
|
||||
<div style="display: flex; flex-direction: column; overflow: hidden; text-align: left;">
|
||||
<span class="user-sfid"
|
||||
style="font-weight: 600; color: #f0f6fc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
{{ userProfile.sfid || 'No SFID Linked' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-menu">
|
||||
|
|
|
|||
|
|
@ -9,6 +9,20 @@ const isInitialized = ref(false)
|
|||
let sessionPollTimer = null
|
||||
|
||||
export const useAuth = () => {
|
||||
const fetchUserProfile = async () => {
|
||||
if (!userToken.value) return
|
||||
try {
|
||||
const profile = await $fetch('/api/user/profile', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${userToken.value}`
|
||||
}
|
||||
})
|
||||
userProfile.value = profile
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch user profile:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const setSession = (data) => {
|
||||
userToken.value = data.access_token
|
||||
if (data.refresh_token) {
|
||||
|
|
@ -43,7 +57,7 @@ export const useAuth = () => {
|
|||
|
||||
setSession(data)
|
||||
window.history.replaceState({}, document.title, window.location.pathname)
|
||||
await verifySession()
|
||||
await fetchUserProfile()
|
||||
startLiveSessionPolling()
|
||||
|
||||
return { success: true, isFirstLogin: data.isFirstLogin }
|
||||
|
|
@ -72,12 +86,9 @@ export const useAuth = () => {
|
|||
})
|
||||
|
||||
setSession(data)
|
||||
const isValid = await verifySession()
|
||||
if (isValid) {
|
||||
startLiveSessionPolling()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
await fetchUserProfile()
|
||||
startLiveSessionPolling()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn('Silent refresh failed:', error)
|
||||
clearSession()
|
||||
|
|
@ -88,7 +99,9 @@ export const useAuth = () => {
|
|||
}
|
||||
|
||||
const verifySession = async () => {
|
||||
if (!userToken.value) return false
|
||||
if (!userToken.value) {
|
||||
return await silentRefresh()
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await $fetch('/api/auth/check-session', {
|
||||
|
|
@ -97,14 +110,18 @@ export const useAuth = () => {
|
|||
})
|
||||
|
||||
if (res.active) {
|
||||
userProfile.value = res.user
|
||||
isAuthenticated.value = true
|
||||
if (!userProfile.value) {
|
||||
await fetchUserProfile()
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return await silentRefresh()
|
||||
}
|
||||
} catch (e) {
|
||||
return await silentRefresh()
|
||||
} finally {
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +173,7 @@ export const useAuth = () => {
|
|||
exchangeCodeForToken,
|
||||
silentRefresh,
|
||||
verifySession,
|
||||
fetchUserProfile,
|
||||
logout
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const publicRoutes = ['/oauth']
|
||||
|
||||
if (publicRoutes.includes(to.path)) {
|
||||
if (to.path === '/oauth') {
|
||||
return
|
||||
}
|
||||
|
||||
const { isAuthenticated, verifySession, isInitialized } = useAuth()
|
||||
if (import.meta.server) {
|
||||
return
|
||||
}
|
||||
|
||||
const { isAuthenticated, isInitialized, verifySession, silentRefresh } = useAuth()
|
||||
|
||||
if (!isInitialized.value) {
|
||||
await verifySession()
|
||||
const hasToken = localStorage.getItem('keycloak_refresh_token')
|
||||
if (hasToken) {
|
||||
await silentRefresh()
|
||||
} else {
|
||||
await verifySession()
|
||||
}
|
||||
}
|
||||
|
||||
//whats the shape of Italy?
|
||||
|
|
|
|||
44
server/api/user/profile.get.ts
Normal file
44
server/api/user/profile.get.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { query } from '../../utils/db'
|
||||
|
||||
function parseJwt(token: string) {
|
||||
try {
|
||||
const base64Payload = token.split('.')[1]
|
||||
return JSON.parse(Buffer.from(base64Payload, 'base64').toString('utf8'))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const authHeader = getRequestHeader(event, 'authorization')
|
||||
const token = authHeader?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Missing authorization header' })
|
||||
}
|
||||
|
||||
const decoded = parseJwt(token)
|
||||
if (!decoded?.sub) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Invalid token' })
|
||||
}
|
||||
|
||||
let dbUser = null
|
||||
try {
|
||||
const result = await query(
|
||||
`SELECT sfid, pid, first_login_completed FROM users WHERE keycloak_id = $1 LIMIT 1`,
|
||||
[decoded.sub]
|
||||
)
|
||||
dbUser = result.rows[0] || null
|
||||
} catch (error) {
|
||||
console.error('[DB PROFILE FETCH ERROR]:', error)
|
||||
}
|
||||
|
||||
return {
|
||||
keycloakId: decoded.sub,
|
||||
username: decoded.preferred_username || decoded.given_name || 'User',
|
||||
email: decoded.email,
|
||||
sfid: dbUser?.sfid || null,
|
||||
pid: dbUser?.pid || null,
|
||||
firstLoginCompleted: dbUser?.first_login_completed || false
|
||||
}
|
||||
})
|
||||
Loading…
Reference in a new issue