a bunch of changes

This commit is contained in:
kittentm 2026-08-02 05:43:09 +02:00
commit 4bd99fd3e5
Signed by: kitten
GPG key ID: 394B4EABE4405A83
5 changed files with 117 additions and 17 deletions

View file

@ -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;

View file

@ -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">

View file

@ -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
}
}

View file

@ -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?

View 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
}
})