entorno de desarrollo listo

This commit is contained in:
2025-10-05 15:56:42 -06:00
parent 4a1f153417
commit 0380f69f1b
7 changed files with 165 additions and 14 deletions

8
.mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest"]
}
}
}

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
const { user, loading, fetchUser, logout } = useAuth()
// Cargar usuario al montar
onMounted(() => {
fetchUser()
})
const items = computed(() => [
[{
label: user.value?.email || 'Usuario',
slot: 'account',
disabled: true
}],
[{
label: 'Cerrar sesión',
icon: 'i-heroicons-arrow-right-on-rectangle',
click: logout
}]
])
</script>
<template>
<UDropdownMenu
v-if="user?.authenticated && !loading"
:items="items"
:ui="{ item: { disabled: 'cursor-text select-text' } }"
:popper="{ placement: 'bottom-start' }"
>
<UAvatar
:alt="user.name || user.username || 'User'"
size="sm"
/>
<template #account="{ item }">
<div class="text-left">
<p class="font-medium text-gray-900 dark:text-white">
{{ user.name || user.username }}
</p>
<p class="text-sm text-gray-500 dark:text-gray-400 truncate">
{{ item.label }}
</p>
</div>
</template>
</UDropdownMenu>
<USkeleton v-else-if="loading" class="h-8 w-8" :ui="{ rounded: 'rounded-full' }" />
</template>

View File

@@ -0,0 +1,38 @@
export interface AuthUser {
username: string | null
email: string | null
name: string | null
uid: string | null
groups: string[]
authenticated: boolean
}
export const useAuth = () => {
const user = useState<AuthUser | null>('auth-user', () => null)
const loading = useState<boolean>('auth-loading', () => false)
const fetchUser = async () => {
loading.value = true
try {
const data = await $fetch<AuthUser>('/api/auth/user')
user.value = data
} catch (error) {
console.error('Error fetching user:', error)
user.value = null
} finally {
loading.value = false
}
}
const logout = () => {
// Authentik maneja el logout, redirigir a la URL de logout
window.location.href = '/outpost.goauthentik.io/sign_out'
}
return {
user: readonly(user),
loading: readonly(loading),
fetchUser,
logout
}
}

View File

@@ -16,6 +16,7 @@
<template #trailing>
<UBadge variant="subtle" label="Supabase" class="uppercase tracking-wide" />
<UBadge variant="subtle" label="Solo lectura" class="uppercase tracking-wide" />
<UserMenu />
</template>
</UDashboardNavbar>
</div>

View File

@@ -17,6 +17,17 @@ export default defineNuxtConfig({
// Optimize build
vite: {
plugins: [disableImportProtection()],
server: {
hmr: {
clientPort: 443,
protocol: 'wss'
},
allowedHosts: [
'.nucleoriofrio.com',
'devserver.nucleoriofrio.com',
'analitica.nucleoriofrio.com'
]
},
build: {
cssCodeSplit: true,
rollupOptions: {
@@ -30,11 +41,12 @@ export default defineNuxtConfig({
}
},
app: {
baseURL: process.env.BASE_URL || '/',
head: {
link: [
{ rel: 'icon', type: 'image/png', href: '/icons/icon-192.png' },
{ rel: 'apple-touch-icon', sizes: '192x192', href: '/icons/icon-192.png' },
{ rel: 'manifest', href: '/manifest.webmanifest' }
{ rel: 'icon', type: 'image/png', href: `${process.env.BASE_URL || ''}/icons/icon-192.png` },
{ rel: 'apple-touch-icon', sizes: '192x192', href: `${process.env.BASE_URL || ''}/icons/icon-192.png` },
{ rel: 'manifest', href: `${process.env.BASE_URL || ''}/manifest.webmanifest` }
],
meta: [
{ name: 'theme-color', content: '#1b1209' },
@@ -45,18 +57,15 @@ export default defineNuxtConfig({
}
},
nitro: {
baseURL: process.env.BASE_URL || '/',
experimental: {
openAPI: true
},
routeRules: {
'/**': {
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin'
}
},
'/manifest.webmanifest': {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Content-Type': 'application/manifest+json'
'Content-Type': 'application/manifest+json',
'Cache-Control': 'public, max-age=3600'
}
},
'/sw.js': {
@@ -76,8 +85,8 @@ export default defineNuxtConfig({
name: 'Analítica Núcleo Data Studio',
short_name: 'Analítica',
description: 'Explora y valida tus tablas Supabase desde un único panel en modo lectura.',
start_url: '/',
scope: '/',
start_url: process.env.BASE_URL || '/',
scope: process.env.BASE_URL || '/',
display: 'standalone',
background_color: '#1b1209',
theme_color: '#c08040',

View File

@@ -0,0 +1,15 @@
export default defineEventHandler((event) => {
const headers = getHeaders(event)
// Authentik envía información del usuario en headers específicos
const user = {
username: headers['x-authentik-username'] || null,
email: headers['x-authentik-email'] || null,
name: headers['x-authentik-name'] || null,
uid: headers['x-authentik-uid'] || null,
groups: headers['x-authentik-groups'] ? headers['x-authentik-groups'].split(',') : [],
authenticated: !!headers['x-authentik-username']
}
return user
})

View File

@@ -0,0 +1,32 @@
export default defineEventHandler((event) => {
const origin = getHeader(event, 'origin')
const path = event.path || ''
// Rutas públicas que siempre permiten CORS desde cualquier origen
const publicRoutes = ['/manifest.webmanifest', '/sw.js', '/workbox-', '/_nuxt/', '/icons/', '/screenshots/']
const isPublicRoute = publicRoutes.some(route => path.startsWith(route))
if (isPublicRoute) {
setHeaders(event, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '86400'
})
} else if (origin && (origin.endsWith('.nucleoriofrio.com') || origin === 'https://nucleoriofrio.com')) {
// Permitir CORS desde cualquier subdominio de .nucleoriofrio.com para otras rutas
setHeaders(event, {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400'
})
}
// Manejar preflight requests
if (getMethod(event) === 'OPTIONS') {
event.node.res.statusCode = 204
event.node.res.end()
}
})