Files
perfil/nuxt4/server/api/avatar/upload.post.ts
josedario87 8109f7e1d0
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 57s
Implementar sistema completo de captura y gestión de avatares
- Agregar CameraCapture.vue con soporte multi-dispositivo
  * Soporte para múltiples cámaras (frontal/trasera)
  * Manejo robusto de permisos y errores
  * Preview y confirmación de foto
  * Detección automática de capacidades del dispositivo

- Crear endpoint /api/avatar/upload para subir avatares
  * Validación de tipo y tamaño de archivo
  * Almacenamiento en /public/avatars/
  * Actualización de atributos en Authentik
  * Limpieza automática de avatares antiguos

- Actualizar UserProfileForm con gestión de avatar
  * Integración con CameraCapture en modal
  * Preview del avatar actual con MsnAvatar
  * Opciones para cambiar y eliminar avatar

- Actualizar useAuthentik para avatares personalizados
  * Carga de atributos completos del usuario
  * Soporte para avatar_url desde Authentik
  * Fallback a UI Avatars si no hay custom avatar
2025-10-17 16:35:59 -06:00

165 lines
4.8 KiB
TypeScript

/**
* API endpoint para subir avatar del usuario
* POST /api/avatar/upload
*
* Recibe una imagen, la guarda en /public/avatars/ y actualiza Authentik
*/
import { promises as fs } from 'fs'
import { join } from 'path'
import { createHash } from 'crypto'
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']
const AVATARS_DIR = 'public/avatars'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const headers = getRequestHeaders(event)
// Verificar autenticación
const username = headers['x-authentik-username']
if (!username) {
throw createError({
statusCode: 401,
message: 'Usuario no autenticado'
})
}
try {
// Leer el body como multipart/form-data
const formData = await readMultipartFormData(event)
if (!formData || formData.length === 0) {
throw createError({
statusCode: 400,
message: 'No se recibió ningún archivo'
})
}
// Buscar el campo 'avatar'
const avatarFile = formData.find(field => field.name === 'avatar')
if (!avatarFile) {
throw createError({
statusCode: 400,
message: 'Campo "avatar" no encontrado'
})
}
// Validar tipo de archivo
const contentType = avatarFile.type || ''
if (!ALLOWED_TYPES.includes(contentType)) {
throw createError({
statusCode: 400,
message: `Tipo de archivo no permitido. Usa: ${ALLOWED_TYPES.join(', ')}`
})
}
// Validar tamaño
if (avatarFile.data.length > MAX_FILE_SIZE) {
throw createError({
statusCode: 400,
message: `Archivo muy grande. Máximo: ${MAX_FILE_SIZE / (1024 * 1024)}MB`
})
}
// Crear directorio de avatares si no existe
const avatarsPath = join(process.cwd(), AVATARS_DIR)
try {
await fs.access(avatarsPath)
} catch {
await fs.mkdir(avatarsPath, { recursive: true })
}
// Generar nombre de archivo único basado en username
const hash = createHash('md5').update(username).digest('hex').substring(0, 8)
const timestamp = Date.now()
const extension = contentType.split('/')[1]
const filename = `${username}-${hash}-${timestamp}.${extension}`
const filepath = join(avatarsPath, filename)
// Guardar archivo
await fs.writeFile(filepath, avatarFile.data)
// URL pública del avatar
const avatarUrl = `/avatars/${filename}`
// Actualizar Authentik con la URL del avatar
const authentikUrl = config.authentikApiUrl || config.public.authentikUrl
const authentikToken = config.authentikApiToken
if (!authentikToken) {
console.warn('⚠️ Token de Authentik no configurado, no se puede actualizar metadatos')
} else {
try {
// Obtener ID del usuario
const usersResponse = await $fetch(`${authentikUrl}/api/v3/core/users/?username=${username}`, {
headers: {
'Authorization': `Bearer ${authentikToken}`,
'Content-Type': 'application/json'
}
})
const users = usersResponse as any
if (users.results && users.results.length > 0) {
const userId = users.results[0].pk
// Actualizar atributos del usuario
await $fetch(`${authentikUrl}/api/v3/core/users/${userId}/`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${authentikToken}`,
'Content-Type': 'application/json'
},
body: {
attributes: {
...users.results[0].attributes,
avatar_url: avatarUrl
}
}
})
console.log(`✅ Avatar actualizado para usuario ${username}: ${avatarUrl}`)
}
} catch (err) {
console.error('Error actualizando Authentik:', err)
// No fallar si Authentik no se puede actualizar, el avatar ya está guardado
}
}
// Limpiar avatares antiguos del usuario (opcional)
try {
const files = await fs.readdir(avatarsPath)
const userFiles = files.filter(f =>
f.startsWith(`${username}-`) &&
f !== filename
)
for (const oldFile of userFiles) {
await fs.unlink(join(avatarsPath, oldFile))
console.log(`🗑️ Eliminado avatar antiguo: ${oldFile}`)
}
} catch (err) {
console.error('Error limpiando avatares antiguos:', err)
}
return {
success: true,
avatarUrl,
message: 'Avatar actualizado correctamente'
}
} catch (error: any) {
console.error('Error en upload de avatar:', error)
// Re-throw si ya es un error de createError
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
message: error.message || 'Error al subir avatar'
})
}
})