- Agregada nueva funcionalidad de "Forgot Password" para reestablecer la contraseña.
- Cambios en estilos del login para mayor consistencia con el resto de la pagina.
- Creado correo con template propio fiel al estilo de la pagina.
This commit is contained in:
Bryam105
2026-05-27 09:50:00 -03:00
parent f03890b348
commit 8ad829d9d2
9 changed files with 349 additions and 20 deletions
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Str;
class NewPasswordController extends Controller
{
public function create(Request $request, $token)
{
return view('auth.reset-password', ['request' => $request, 'token' => $token]);
}
public function store(Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|min:8|confirmed',
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->forceFill([
'password' => Hash::make($password)
])->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
}
);
if ($status == Password::PASSWORD_RESET) {
return redirect()->route('login')->with('status', __($status));
}
return back()->withErrors(['email' => [__($status)]]);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class PasswordResetLinkController extends Controller
{
public function create()
{
return view('auth.forgot-password');
}
public function store(Request $request)
{
$request->validate([
'email' => 'required|email',
]);
$status = Password::sendResetLink(
$request->only('email')
);
if ($status == Password::RESET_LINK_SENT) {
return back()->with('status', __($status));
}
return back()->withErrors(['email' => __($status)]);
}
}
+12
View File
@@ -6,6 +6,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use App\Notifications\ResetPasswordNotification;
class User extends Authenticatable class User extends Authenticatable
{ {
@@ -45,4 +46,15 @@ class User extends Authenticatable
'password' => 'hashed', 'password' => 'hashed',
]; ];
} }
/**
* Send the password reset notification.
*
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPasswordNotification($token));
}
} }
@@ -0,0 +1,26 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Auth\Notifications\ResetPassword as DefaultResetPassword;
class ResetPasswordNotification extends DefaultResetPassword
{
use Queueable;
public function toMail($notifiable)
{
$url = url(route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
return (new MailMessage)
->subject('Recuperación de Contraseña - Lauck')
->view('emails.reset-password', [
'url' => $url,
]);
}
}
@@ -0,0 +1,51 @@
<x-layout title="Recuperar Contraseña - Lauck">
<div class="w-full flex flex-col items-center justify-center pt-8">
<x-section-header
subtitle="Recuperación de cuenta"
title="Bicicletería "
highlight="Lauck"
/>
<div class="bg-gray-100 dark:bg-panel-bg p-8 rounded-xl shadow-lg w-full max-w-md border border-gray-300 dark:border-neutral-700">
<div class="mb-6 text-sm text-gray-600 dark:text-gray-400">
¿Olvidaste tu contraseña? No hay problema. Simplemente déjanos saber tu dirección de correo electrónico y te enviaremos un enlace para restablecer la contraseña que te permitirá elegir una nueva.
</div>
@if (session('status'))
<div class="mb-4 font-medium text-sm text-green-600 dark:text-neon-lime bg-green-100 dark:bg-green-900/30 p-3 rounded-lg border border-green-500/50">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('password.email') }}" class="space-y-6">
@csrf
@if ($errors->any())
<div class="bg-red-100 dark:bg-red-900/50 border border-red-500 text-red-600 dark:text-red-300 p-3 rounded-lg shadow-sm">
<p class="text-sm font-medium">
{{ $errors->first() }}
</p>
</div>
@endif
<!-- Email Address -->
<div>
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">Correo electrónico</label>
<input type="email" name="email" value="{{ old('email') }}" required autofocus
class="w-full px-4 py-2 bg-white dark:bg-neutral-900 border border-gray-300 dark:border-neutral-700 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-neon-lime focus:border-neon-lime transition-colors"/>
</div>
<div class="flex items-center justify-between mt-4">
<a class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors" href="{{ route('login') }}">
Volver a inicio de sesión
</a>
<button type="submit"
class="px-4 py-2 bg-neutral-900 dark:bg-neon-lime text-white dark:text-neutral-900 font-bold rounded-lg hover:bg-neutral-800 hover:dark:bg-[#b3e600] transition-colors shadow-lg dark:shadow-neon-lime/20">
Enviar enlace
</button>
</div>
</form>
</div>
</div>
</x-layout>
@@ -0,0 +1,56 @@
<x-layout title="Restablecer Contraseña - Lauck">
<div class="w-full flex flex-col items-center justify-center pt-8">
<x-section-header
subtitle="Nueva contraseña"
title="Bicicletería "
highlight="Lauck"
/>
<div class="bg-gray-100 dark:bg-panel-bg p-8 rounded-xl shadow-lg w-full max-w-md border border-gray-300 dark:border-neutral-700">
<form method="POST" action="{{ route('password.store') }}" class="space-y-6">
@csrf
<!-- Password Reset Token -->
<input type="hidden" name="token" value="{{ $token }}">
@if ($errors->any())
<div class="bg-red-100 dark:bg-red-900/50 border border-red-500 text-red-600 dark:text-red-300 p-3 rounded-lg shadow-sm">
<ul class="text-sm font-medium list-disc list-inside">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<!-- Email Address -->
<div>
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">Correo electrónico</label>
<input type="email" name="email" value="{{ old('email', $request->email) }}" required autofocus
class="w-full px-4 py-2 bg-gray-200 dark:bg-neutral-800 border border-gray-300 dark:border-neutral-700 text-gray-700 dark:text-gray-400 rounded-lg focus:outline-none" readonly />
</div>
<!-- Password -->
<div>
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">Nueva Contraseña</label>
<input type="password" name="password" required
class="w-full px-4 py-2 bg-white dark:bg-neutral-900 border border-gray-300 dark:border-neutral-700 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-neon-lime focus:border-neon-lime transition-colors"/>
</div>
<!-- Confirm Password -->
<div>
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">Confirmar Contraseña</label>
<input type="password" name="password_confirmation" required
class="w-full px-4 py-2 bg-white dark:bg-neutral-900 border border-gray-300 dark:border-neutral-700 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-neon-lime focus:border-neon-lime transition-colors"/>
</div>
<div class="flex items-center justify-end mt-4">
<button type="submit"
class="w-full py-2.5 bg-neutral-900 dark:bg-neon-lime text-neon-lime dark:text-neutral-900 font-bold rounded-lg hover:bg-neutral-800 hover:dark:bg-[#b3e600] transition-colors shadow-lg dark:shadow-neon-lime/20 tracking-wide uppercase">
Restablecer Contraseña
</button>
</div>
</form>
</div>
</div>
</x-layout>
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Recuperar Contraseña</title>
</head>
<body style="margin: 0; padding: 0; background-color: #1a1a1a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: #ffffff;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #1a1a1a; width: 100%;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table width="100%" max-width="600" cellpadding="0" cellspacing="0" border="0" style="max-width: 600px; width: 100%; background-color: #242424; border-radius: 12px; border: 1px solid #333333; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);">
<!-- Header -->
<tr>
<td align="center" style="padding: 40px 20px 20px 20px; border-bottom: 1px solid #333333;">
<h1 style="margin: 0; font-size: 24px; font-weight: 800; color: #ffffff;">
Bicicletería <span style="color: #b3e600;">Lauck</span>
</h1>
<p style="margin: 5px 0 0 0; font-size: 14px; color: #a0a0a0; text-transform: uppercase; letter-spacing: 1px;">Recuperación de cuenta</p>
</td>
</tr>
<!-- Body -->
<tr>
<td style="padding: 40px 30px;">
<p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.5; color: #e5e5e5;">
Hola,
</p>
<p style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.5; color: #a0a0a0;">
Estás recibiendo este correo porque recibimos una solicitud de restablecimiento de contraseña para tu cuenta en <strong>Lauck</strong>.
</p>
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td align="center">
<a href="{{ $url }}" style="display: inline-block; padding: 14px 30px; background-color: #b3e600; color: #1a1a1a; text-decoration: none; font-size: 16px; font-weight: bold; border-radius: 8px; text-transform: uppercase; letter-spacing: 0.5px;">
Restablecer Contraseña
</a>
</td>
</tr>
</table>
<p style="margin: 30px 0 0 0; font-size: 16px; line-height: 1.5; color: #a0a0a0;">
Este enlace para restablecer la contraseña caducará en 60 minutos.
</p>
<p style="margin: 20px 0 0 0; font-size: 16px; line-height: 1.5; color: #a0a0a0;">
Si no solicitaste un restablecimiento de contraseña, no es necesario realizar ninguna otra acción.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #1f1f1f; padding: 20px 30px; border-top: 1px solid #333333;">
<p style="margin: 0; font-size: 12px; line-height: 1.5; color: #666666; text-align: center;">
Si tienes problemas para hacer clic en el botón "Restablecer Contraseña", copia y pega la siguiente URL en tu navegador web:<br>
<a href="{{ $url }}" style="color: #b3e600; word-break: break-all; margin-top: 10px; display: inline-block;">{{ $url }}</a>
</p>
</td>
</tr>
</table>
<p style="margin: 20px 0 0 0; font-size: 12px; color: #666666; text-align: center;">
© {{ date('Y') }} Bicicletería Lauck. Todos los derechos reservados.
</p>
</td>
</tr>
</table>
</body>
</html>
+45 -24
View File
@@ -1,44 +1,65 @@
<x-layout title="Login - Lauck"> <x-layout title="Login - Lauck">
<div class="justify-center"> <div class="w-full flex flex-col items-center justify-center pt-8">
<x-section-header <x-section-header
subtitle="Inicio de sesion" subtitle="Inicio de sesión"
title="Bicicletería " title="Bicicletería "
highlight="Lauck" highlight="Lauck"
/> />
<div class="bg-panel-bg p-8 rounded-xl shadow-lg w-full max-w-md border border-neutral-700">
<form action="{{ route('login.attempt') }}" method="POST" class="space-y-5"> <div class="bg-gray-100 dark:bg-panel-bg p-8 rounded-xl shadow-lg w-full max-w-md border border-gray-300 dark:border-neutral-700">
<form action="{{ route('login.attempt') }}" method="POST" class="space-y-6">
@csrf @csrf
{{-- Bloque de errores --}} {{-- Bloque de errores --}}
@if ($errors->any()) @if ($errors->any())
<div class="bg-red-900 border border-red-700 text-white p-3 rounded-md"> <div class="bg-red-100 dark:bg-red-900/50 border border-red-500 text-red-600 dark:text-red-300 p-3 rounded-lg shadow-sm">
<p class="text-sm font-medium"> <p class="text-sm font-medium">
{{ $errors->first() }} {{ $errors->first() }}
</p> </p>
</div> </div>
@endif @endif
<div> @if (session('status'))
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">Correo electrónico</x-forms.label> <div class="bg-green-100 dark:bg-green-900/50 border border-green-500 text-green-700 dark:text-green-300 p-3 rounded-lg shadow-sm">
<x-forms.input type="email" name="email" required <p class="text-sm font-medium">
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600"/> {{ session('status') }}
</div>
<div>
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">Contraseña</x-forms.label>
<x-forms.input type="password" name="password" required
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600"/>
</div>
<div>
<input type="submit" name="submit" value="Iniciar sesión"
class="w-full bg-lime-400 font-bold text-white py-2 rounded-md hover:bg-lime-500 transition-colors">
</div>
<p class="text-center text-sm text-gray-600">
¿No tienes una cuenta? <a href="register" class="text-blue-600 hover:underline">Regístrate acá</a>.
</p> </p>
</div>
@endif
<div>
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">Correo electrónico</label>
<input type="email" name="email" required autofocus
class="w-full px-4 py-2 bg-white dark:bg-neutral-900 border border-gray-300 dark:border-neutral-700 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-neon-lime focus:border-neon-lime transition-colors"/>
</div>
<div>
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">Contraseña</label>
<input type="password" name="password" required
class="w-full px-4 py-2 bg-white dark:bg-neutral-900 border border-gray-300 dark:border-neutral-700 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-neon-lime focus:border-neon-lime transition-colors"/>
<div class="flex justify-end mt-2">
<a href="{{ route('password.request') }}" class="text-xs font-medium text-green-600 dark:text-neon-lime hover:underline transition-colors">
¿Olvidaste tu contraseña?
</a>
</div>
</div>
<div>
<button type="submit"
class="w-full bg-neutral-900 dark:bg-neon-lime text-neon-lime dark:text-neutral-900 font-bold py-2.5 rounded-lg hover:bg-neutral-800 hover:dark:bg-[#b3e600] transition-colors shadow-lg dark:shadow-neon-lime/20 uppercase tracking-wide">
Iniciar sesión
</button>
</div>
<div class="text-center mt-6 pt-4 border-t border-gray-300 dark:border-neutral-700">
<p class="text-sm text-gray-600 dark:text-gray-400">
¿No tienes una cuenta? <a href="{{ route('register') }}" class="font-bold text-gray-900 dark:text-white hover:text-green-600 dark:hover:text-neon-lime transition-colors">Regístrate acá</a>.
</p>
</div>
</form> </form>
</div> </div>
</div> </div>
</x-layout> </x-layout>
+16
View File
@@ -13,6 +13,8 @@ use App\Http\Controllers\TallerController;
use App\Http\Controllers\AgendaController; use App\Http\Controllers\AgendaController;
use App\Http\Controllers\AppointmentController; use App\Http\Controllers\AppointmentController;
use App\Http\Controllers\SupplierController; use App\Http\Controllers\SupplierController;
use App\Http\Controllers\PasswordResetLinkController;
use App\Http\Controllers\NewPasswordController;
use App\Models\Product; use App\Models\Product;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
@@ -25,6 +27,20 @@ Route::resource('catalogo', CatalogoController::class)->only(['index', 'show'])-
Route::get('login', function(){ return view('login'); })->name('login'); Route::get('login', function(){ return view('login'); })->name('login');
Route::post('login', LoginController::class)->middleware('throttle:5,1')->name('login.attempt'); Route::post('login', LoginController::class)->middleware('throttle:5,1')->name('login.attempt');
Route::middleware('guest')->group(function () {
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
->name('password.email');
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
->name('password.reset');
Route::post('reset-password', [NewPasswordController::class, 'store'])
->name('password.store');
});
Route::view('register', 'register')->name('register'); Route::view('register', 'register')->name('register');
Route::post('register', RegisterController::class)->name('register.store'); Route::post('register', RegisterController::class)->name('register.store');