NUEVO:
- 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:
@@ -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)]);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use App\Notifications\ResetPasswordNotification;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
@@ -45,4 +46,15 @@ class User extends Authenticatable
|
||||
'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>
|
||||
@@ -1,44 +1,65 @@
|
||||
<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
|
||||
subtitle="Inicio de sesion"
|
||||
subtitle="Inicio de sesión"
|
||||
title="Bicicletería "
|
||||
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
|
||||
|
||||
{{-- Bloque de errores --}}
|
||||
@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">
|
||||
{{ $errors->first() }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">Correo electrónico</x-forms.label>
|
||||
<x-forms.input type="email" name="email" 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>
|
||||
<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>.
|
||||
@if (session('status'))
|
||||
<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">
|
||||
<p class="text-sm font-medium">
|
||||
{{ session('status') }}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-layout>
|
||||
@@ -13,6 +13,8 @@ use App\Http\Controllers\TallerController;
|
||||
use App\Http\Controllers\AgendaController;
|
||||
use App\Http\Controllers\AppointmentController;
|
||||
use App\Http\Controllers\SupplierController;
|
||||
use App\Http\Controllers\PasswordResetLinkController;
|
||||
use App\Http\Controllers\NewPasswordController;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
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::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::post('register', RegisterController::class)->name('register.store');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user