entorno de desarrollo listo
This commit is contained in:
8
.mcp.json
Normal file
8
.mcp.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"chrome-devtools": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "chrome-devtools-mcp@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
nuxt4-app/app/components/UserMenu.vue
Normal file
48
nuxt4-app/app/components/UserMenu.vue
Normal 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>
|
||||||
38
nuxt4-app/app/composables/useAuth.ts
Normal file
38
nuxt4-app/app/composables/useAuth.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
<template #trailing>
|
<template #trailing>
|
||||||
<UBadge variant="subtle" label="Supabase" class="uppercase tracking-wide" />
|
<UBadge variant="subtle" label="Supabase" class="uppercase tracking-wide" />
|
||||||
<UBadge variant="subtle" label="Solo lectura" class="uppercase tracking-wide" />
|
<UBadge variant="subtle" label="Solo lectura" class="uppercase tracking-wide" />
|
||||||
|
<UserMenu />
|
||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ export default defineNuxtConfig({
|
|||||||
// Optimize build
|
// Optimize build
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [disableImportProtection()],
|
plugins: [disableImportProtection()],
|
||||||
|
server: {
|
||||||
|
hmr: {
|
||||||
|
clientPort: 443,
|
||||||
|
protocol: 'wss'
|
||||||
|
},
|
||||||
|
allowedHosts: [
|
||||||
|
'.nucleoriofrio.com',
|
||||||
|
'devserver.nucleoriofrio.com',
|
||||||
|
'analitica.nucleoriofrio.com'
|
||||||
|
]
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
cssCodeSplit: true,
|
cssCodeSplit: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
@@ -30,11 +41,12 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
baseURL: process.env.BASE_URL || '/',
|
||||||
head: {
|
head: {
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/png', href: '/icons/icon-192.png' },
|
{ rel: 'icon', type: 'image/png', href: `${process.env.BASE_URL || ''}/icons/icon-192.png` },
|
||||||
{ rel: 'apple-touch-icon', sizes: '192x192', href: '/icons/icon-192.png' },
|
{ rel: 'apple-touch-icon', sizes: '192x192', href: `${process.env.BASE_URL || ''}/icons/icon-192.png` },
|
||||||
{ rel: 'manifest', href: '/manifest.webmanifest' }
|
{ rel: 'manifest', href: `${process.env.BASE_URL || ''}/manifest.webmanifest` }
|
||||||
],
|
],
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'theme-color', content: '#1b1209' },
|
{ name: 'theme-color', content: '#1b1209' },
|
||||||
@@ -45,18 +57,15 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
nitro: {
|
nitro: {
|
||||||
|
baseURL: process.env.BASE_URL || '/',
|
||||||
|
experimental: {
|
||||||
|
openAPI: true
|
||||||
|
},
|
||||||
routeRules: {
|
routeRules: {
|
||||||
'/**': {
|
|
||||||
headers: {
|
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'/manifest.webmanifest': {
|
'/manifest.webmanifest': {
|
||||||
headers: {
|
headers: {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Content-Type': 'application/manifest+json',
|
||||||
'Access-Control-Allow-Methods': 'GET',
|
'Cache-Control': 'public, max-age=3600'
|
||||||
'Content-Type': 'application/manifest+json'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'/sw.js': {
|
'/sw.js': {
|
||||||
@@ -76,8 +85,8 @@ export default defineNuxtConfig({
|
|||||||
name: 'Analítica Núcleo Data Studio',
|
name: 'Analítica Núcleo Data Studio',
|
||||||
short_name: 'Analítica',
|
short_name: 'Analítica',
|
||||||
description: 'Explora y valida tus tablas Supabase desde un único panel en modo lectura.',
|
description: 'Explora y valida tus tablas Supabase desde un único panel en modo lectura.',
|
||||||
start_url: '/',
|
start_url: process.env.BASE_URL || '/',
|
||||||
scope: '/',
|
scope: process.env.BASE_URL || '/',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#1b1209',
|
background_color: '#1b1209',
|
||||||
theme_color: '#c08040',
|
theme_color: '#c08040',
|
||||||
|
|||||||
15
nuxt4-app/server/api/auth/user.get.ts
Normal file
15
nuxt4-app/server/api/auth/user.get.ts
Normal 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
|
||||||
|
})
|
||||||
32
nuxt4-app/server/middleware/cors.ts
Normal file
32
nuxt4-app/server/middleware/cors.ts
Normal 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user