Implementar Web Share Target API para compartir fotos con la PWA
Some checks failed
build-and-deploy / build-and-deploy (push) Has been cancelled
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:
75
nuxt4/server/api/share-target.post.ts
Normal file
75
nuxt4/server/api/share-target.post.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
68
nuxt4/server/api/share-target/cleanup.post.ts
Normal file
68
nuxt4/server/api/share-target/cleanup.post.ts
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user