Implementar Web Share Target API para compartir fotos con la PWA
Some checks failed
build-and-deploy / build-and-deploy (push) Has been cancelled

- Agregar share_target al manifest de la PWA
- Crear endpoint /api/share-target para recibir archivos compartidos
- Guardar archivos temporalmente en /public/temp-shared
- Modificar UserProfileForm para aceptar imágenes externas
- Detectar automáticamente imágenes compartidas y procesarlas
- Crear endpoint /api/share-target/cleanup para limpiar temporales
- Mostrar toast informativo al recibir imagen compartida
- Redirigir automáticamente al formulario de perfil
- Soportar compartir desde galería, otras apps, etc.
This commit is contained in:
2025-10-17 18:29:00 -06:00
parent ced637a7a9
commit 5bb5e5092e
17 changed files with 367 additions and 1 deletions

View File

@@ -0,0 +1,75 @@
/**
* API endpoint para Web Share Target
* POST /api/share-target
*
* Recibe imágenes compartidas desde otras apps y las almacena temporalmente
*/
import { promises as fs } from 'fs'
import { join } from 'path'
import { createHash, randomBytes } from 'crypto'
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/jpg']
const TEMP_DIR = 'public/temp-shared'
export default defineEventHandler(async (event) => {
try {
// Leer el body como multipart/form-data
const formData = await readMultipartFormData(event)
if (!formData || formData.length === 0) {
// No hay archivo, redirigir a inicio
return sendRedirect(event, '/', 302)
}
// Buscar el campo 'media' (nombre definido en manifest)
const mediaFile = formData.find(field => field.name === 'media')
if (!mediaFile) {
// No hay imagen, redirigir a inicio
return sendRedirect(event, '/', 302)
}
// Validar tipo de archivo
const contentType = mediaFile.type || ''
if (!ALLOWED_TYPES.includes(contentType)) {
console.error('Tipo de archivo no permitido:', contentType)
return sendRedirect(event, '/', 302)
}
// Validar tamaño
if (mediaFile.data.length > MAX_FILE_SIZE) {
console.error('Archivo muy grande:', mediaFile.data.length)
return sendRedirect(event, '/', 302)
}
// Crear directorio temporal si no existe
const tempPath = join(process.cwd(), TEMP_DIR)
try {
await fs.access(tempPath)
} catch {
await fs.mkdir(tempPath, { recursive: true })
}
// Generar token único para este archivo temporal
const token = randomBytes(16).toString('hex')
const timestamp = Date.now()
const extension = contentType.split('/')[1]
const filename = `shared-${token}-${timestamp}.${extension}`
const filepath = join(tempPath, filename)
// Guardar archivo temporal
await fs.writeFile(filepath, mediaFile.data)
// URL pública del archivo temporal
const tempUrl = `/temp-shared/${filename}`
console.log(`✅ Imagen compartida guardada temporalmente: ${tempUrl}`)
// Redirigir a la página principal con el token
return sendRedirect(event, `/?shared=${token}&file=${encodeURIComponent(filename)}`, 302)
} catch (error: any) {
console.error('Error en share-target:', error)
return sendRedirect(event, '/', 302)
}
})

View File

@@ -0,0 +1,68 @@
/**
* API endpoint para limpiar archivos temporales de Web Share Target
* POST /api/share-target/cleanup
*
* Elimina archivos temporales después de ser procesados
*/
import { promises as fs } from 'fs'
import { join } from 'path'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const { imageUrl } = body
if (!imageUrl || typeof imageUrl !== 'string') {
throw createError({
statusCode: 400,
message: 'imageUrl es requerido'
})
}
// Extraer el filename de la URL (/temp-shared/filename.jpg)
const filename = imageUrl.split('/').pop()
if (!filename || !filename.startsWith('shared-')) {
throw createError({
statusCode: 400,
message: 'URL de imagen inválida'
})
}
// Construir ruta completa del archivo
const filepath = join(process.cwd(), 'public', 'temp-shared', filename)
// Intentar eliminar el archivo
try {
await fs.unlink(filepath)
console.log(`🗑️ Archivo temporal eliminado: ${filename}`)
return {
success: true,
message: 'Archivo temporal eliminado correctamente'
}
} catch (err: any) {
// Si el archivo no existe, no es un error crítico
if (err.code === 'ENOENT') {
console.log(`⚠️ Archivo temporal ya no existe: ${filename}`)
return {
success: true,
message: 'Archivo ya había sido eliminado'
}
}
throw err
}
} catch (error: any) {
console.error('Error en cleanup de share-target:', error)
// Re-throw si ya es un error de createError
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
message: error.message || 'Error al eliminar archivo temporal'
})
}
})