diff --git a/back/app/Http/Controllers/Administracion/NoticiaController.php b/back/app/Http/Controllers/Administracion/NoticiaController.php index d7aa0ef..03bf1dc 100644 --- a/back/app/Http/Controllers/Administracion/NoticiaController.php +++ b/back/app/Http/Controllers/Administracion/NoticiaController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Administracion; use App\Http\Controllers\Controller; - use App\Models\Noticia; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; @@ -18,13 +17,12 @@ class NoticiaController extends Controller $query = Noticia::query(); - // filtros opcionales if ($request->filled('publicado')) { $query->where('publicado', $request->boolean('publicado')); } if ($request->filled('categoria')) { - $query->where('categoria', $request->string('categoria')); + $query->where('categoria', (string) $request->get('categoria')); } if ($request->filled('q')) { @@ -44,7 +42,7 @@ class NoticiaController extends Controller return response()->json([ 'success' => true, - 'data' => $data->items(), + 'data' => $data->items(), // incluye imagen_url por el accessor/appends 'meta' => [ 'current_page' => $data->currentPage(), 'last_page' => $data->lastPage(), @@ -59,22 +57,22 @@ class NoticiaController extends Controller { return response()->json([ 'success' => true, - 'data' => $noticia, + 'data' => $noticia, // incluye imagen_url por el accessor/appends ]); } - // GET /api/noticias/{noticia} -public function showPublic(Noticia $noticia) -{ - abort_unless($noticia->publicado, 404); - return response()->json([ - 'success' => true, - 'data' => $noticia, - ]); -} + // GET /api/noticias-publicas/{noticia} (o la ruta que uses) + public function showPublic(Noticia $noticia) + { + abort_unless($noticia->publicado, 404); + return response()->json([ + 'success' => true, + 'data' => $noticia, + ]); + } - // POST /api/noticias (multipart/form-data si viene imagen) + // POST /api/noticias (multipart/form-data si viene imagen) public function store(Request $request) { $data = $request->validate([ @@ -84,9 +82,12 @@ public function showPublic(Noticia $noticia) 'contenido' => ['nullable', 'string'], 'categoria' => ['nullable', 'string', 'max:80'], 'tag_color' => ['nullable', 'string', 'max:30'], + + // ✅ dos formas de imagen 'imagen' => ['nullable', 'file', 'mimes:jpg,jpeg,png,webp', 'max:4096'], - 'imagen_path' => ['nullable', 'string', 'max:255'], - 'link_url' => ['nullable', 'string', 'max:600'], + 'imagen_url' => ['nullable', 'url', 'max:600'], + + 'link_url' => ['nullable', 'url', 'max:600'], 'link_texto' => ['nullable', 'string', 'max:120'], 'fecha_publicacion' => ['nullable', 'date'], 'publicado' => ['nullable', 'boolean'], @@ -94,15 +95,19 @@ public function showPublic(Noticia $noticia) 'orden' => ['nullable', 'integer'], ]); - // slug por defecto + // slug por defecto (igual tu modelo lo genera, pero aquí lo dejamos por consistencia) if (empty($data['slug'])) { $data['slug'] = Str::slug($data['titulo']); } - // subir imagen si viene + // si viene archivo, manda a storage y prioriza archivo if ($request->hasFile('imagen')) { $path = $request->file('imagen')->store('noticias', 'public'); $data['imagen_path'] = $path; + $data['imagen_url'] = null; // ✅ evita conflicto con url externa + } else { + // si viene imagen_url externa, no debe haber imagen_path + $data['imagen_path'] = null; } // si publican sin fecha, poner ahora @@ -114,7 +119,7 @@ public function showPublic(Noticia $noticia) return response()->json([ 'success' => true, - 'data' => $noticia, + 'data' => $noticia->fresh(), ], 201); } @@ -128,9 +133,12 @@ public function showPublic(Noticia $noticia) 'contenido' => ['sometimes', 'nullable', 'string'], 'categoria' => ['sometimes', 'nullable', 'string', 'max:80'], 'tag_color' => ['sometimes', 'nullable', 'string', 'max:30'], + + // ✅ dos formas de imagen 'imagen' => ['sometimes', 'nullable', 'file', 'mimes:jpg,jpeg,png,webp', 'max:4096'], - 'imagen_path' => ['sometimes', 'nullable', 'string', 'max:255'], - 'link_url' => ['sometimes', 'nullable', 'string', 'max:600'], + 'imagen_url' => ['sometimes', 'nullable', 'url', 'max:600'], + + 'link_url' => ['sometimes', 'nullable', 'url', 'max:600'], 'link_texto' => ['sometimes', 'nullable', 'string', 'max:120'], 'fecha_publicacion' => ['sometimes', 'nullable', 'date'], 'publicado' => ['sometimes', 'boolean'], @@ -138,17 +146,36 @@ public function showPublic(Noticia $noticia) 'orden' => ['sometimes', 'integer'], ]); - // si llega imagen, reemplazar + // Si llega imagen archivo, reemplaza la anterior y limpia imagen_url externa if ($request->hasFile('imagen')) { if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) { Storage::disk('public')->delete($noticia->imagen_path); } $path = $request->file('imagen')->store('noticias', 'public'); + $data['imagen_path'] = $path; + $data['imagen_url'] = null; // ✅ prioridad archivo } + // Si llega imagen_url (externa), borra la imagen física anterior y limpia imagen_path + if (array_key_exists('imagen_url', $data) && !empty($data['imagen_url'])) { + if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) { + Storage::disk('public')->delete($noticia->imagen_path); + } + $data['imagen_path'] = null; + } + + // Si explícitamente mandan imagen_url = null (quitar url) y no mandan archivo, + // no tocamos imagen_path (se queda como está). Si quieres que también limpie path, + // dime y lo cambiamos. + // si se marca publicado y no hay fecha, set now - if (array_key_exists('publicado', $data) && $data['publicado'] && empty($noticia->fecha_publicacion) && empty($data['fecha_publicacion'])) { + if ( + array_key_exists('publicado', $data) && + $data['publicado'] && + empty($noticia->fecha_publicacion) && + empty($data['fecha_publicacion']) + ) { $data['fecha_publicacion'] = now(); } @@ -168,7 +195,6 @@ public function showPublic(Noticia $noticia) // DELETE /api/noticias/{noticia} public function destroy(Noticia $noticia) { - // opcional: borrar imagen al eliminar if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) { Storage::disk('public')->delete($noticia->imagen_path); } diff --git a/back/app/Models/Noticia.php b/back/app/Models/Noticia.php index e748d96..1a61219 100644 --- a/back/app/Models/Noticia.php +++ b/back/app/Models/Noticia.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; class Noticia extends Model @@ -20,6 +21,7 @@ class Noticia extends Model 'categoria', 'tag_color', 'imagen_path', + 'imagen_url', // ✅ agrega esto si también lo guardas en BD 'link_url', 'link_texto', 'fecha_publicacion', @@ -35,15 +37,24 @@ class Noticia extends Model 'orden' => 'integer', ]; + // ✅ se incluirá en el JSON protected $appends = ['imagen_url']; public function getImagenUrlAttribute(): ?string { - if (!$this->imagen_path) return null; - return asset('storage/' . ltrim($this->imagen_path, '/')); + // 1) Si en BD hay una URL externa, úsala + if (!empty($this->attributes['imagen_url'])) { + return $this->attributes['imagen_url']; + } + + // 2) Si hay imagen en storage, genera URL absoluta + if (!empty($this->imagen_path)) { + return url(Storage::disk('public')->url($this->imagen_path)); + } + + return null; } - // Auto-generar slug si no viene protected static function booted(): void { static::saving(function (Noticia $noticia) { diff --git a/back/routes/api.php b/back/routes/api.php index ce58896..6348a83 100644 --- a/back/routes/api.php +++ b/back/routes/api.php @@ -79,7 +79,7 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { Route::delete('/noticias/{noticia}', [NoticiaController::class, 'destroy']); }); Route::get('/noticias', [NoticiaController::class, 'index']); -Route::get('/noticias/{noticia}', [NoticiaController::class, 'showPublic']); +Route::get('/noticias/{noticia:slug}', [NoticiaController::class, 'showPublic']); Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { diff --git a/front/src/axios.js b/front/src/axios.js index 5e726ea..9e54b0e 100644 --- a/front/src/axios.js +++ b/front/src/axios.js @@ -9,7 +9,7 @@ const api = axios.create({ } }); -// Request interceptor + api.interceptors.request.use( (config) => { const token = localStorage.getItem('token') @@ -25,7 +25,7 @@ api.interceptors.request.use( } ) -// Response interceptor + api.interceptors.response.use( (response) => { return response @@ -33,21 +33,19 @@ api.interceptors.response.use( async (error) => { const originalRequest = error.config - // Si el error es 401 y no es un intento de re-autenticación + if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true - // Limpiar autenticación + localStorage.removeItem('token') localStorage.removeItem('user') - - // Redirigir a login - router.push('/login') + + router.push('account/auth/login') return Promise.reject(error) } - - // Manejar otros errores + if (error.response?.status === 403) { router.push('/unauthorized') } diff --git a/front/src/components/Footer.vue b/front/src/components/Footer.vue index 4c841bf..3e71c3d 100644 --- a/front/src/components/Footer.vue +++ b/front/src/components/Footer.vue @@ -13,7 +13,7 @@
@@ -40,8 +40,8 @@