ADD: Recuperación de contraseña

This commit is contained in:
Moisés de Jesús Cortés Castellanos 2025-01-06 10:23:11 -06:00
parent e487aa8726
commit a076db9521
16 changed files with 235 additions and 127 deletions

View File

@ -23,7 +23,7 @@ class Broadcast extends Command
*
* @var string
*/
protected $description = 'Command description';
protected $description = 'Servicio de broadcast';
/**
* Execute the console command.

View File

@ -25,7 +25,7 @@ class DownSecure extends Command
*
* @var string
*/
protected $signature = 'app:secure';
protected $signature = 'down:secure';
/**
* The console command description.

View File

@ -6,10 +6,13 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Requests\User\ForgotRequest;
use App\Http\Requests\User\ResetPasswordRequest;
use App\Models\ResetPassword;
use App\Models\User;
use App\Notifications\ForgotPasswordNotification;
use Illuminate\Support\Facades\Log;
use Notsoweb\ApiResponse\Enums\ApiResponse;
use Ramsey\Uuid\Uuid;
/**
* Controlador de sesiones
@ -61,7 +64,9 @@ public function forgotPassword(ForgotRequest $request)
$user = User::where('email', $data['email'])->first();
try {
$user->notify(new ForgotPasswordNotification());
$token = $this->generateToken($user);
$user->notify(new ForgotPasswordNotification($token));
return ApiResponse::OK->response([
'is_sent' => true
@ -75,4 +80,66 @@ public function forgotPassword(ForgotRequest $request)
]);
}
}
}
/**
* Resetear contraseña
*/
public function resetPassword(ResetPasswordRequest $request)
{
$data = $request->validated();
$model = ResetPassword::with('user')->where('token', $data['token'])->first();
if(!$model){
return ApiResponse::UNPROCESSABLE_CONTENT->response([
'token' => [__('auth.token.not_exists')]
]);
}
$expires = $model->created_at->addMinutes(15);
if($expires < now()){
$this->deleteToken($data['token']);
return ApiResponse::UNPROCESSABLE_CONTENT->response([
'token' => [__('auth.token.expired')]
]);
}
$model->user->update([
'password' => bcrypt($data['password']),
]);
$this->deleteToken($data['token']);
return ApiResponse::OK->response([
'is_updated' => true
]);
}
/**
* Generar token
*/
private function generateToken($user)
{
if($user->resetPasswords()->exists()){
$user->resetPasswords()->delete();
}
$token = Uuid::uuid4()->toString();
$user->resetPasswords()->create([
'token' => $token,
]);
return $token;
}
/**
* Eliminar tokens
*/
private function deleteToken($token)
{
ResetPassword::where('token', $token)->delete();
}
}

View File

@ -0,0 +1,45 @@
<?php namespace App\Http\Requests\User;
/**
* @copyright (c) 2024 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use Illuminate\Foundation\Http\FormRequest;
/**
* Solicitud de olvido de contraseña
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class ResetPasswordRequest extends FormRequest
{
/**
* Determinar si el usuario está autorizado para realizar esta solicitud
*/
public function authorize(): bool
{
return true;
}
/**
* Obtener las reglas de validación que se aplican a la solicitud
*/
public function rules(): array
{
return [
'token' => ['required', 'string', 'exists:reset_passwords,token'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
];
}
/**
* Mensajes de validación
*/
public function messages(): array
{
return [
'token.exists' => __('auth.token.both'),
];
}
}

View File

@ -18,8 +18,21 @@ class ResetPassword extends Model
* Atributos asignables
*/
protected $fillable = [
'email',
'user_id',
'token',
'created_at',
];
/**
* Desactivar fecha actualización
*/
const UPDATED_AT = null;
/**
* Un reset de contraseña pertenece a un usuario
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -116,4 +116,12 @@ public function validateForPassportPasswordGrant(string $password): bool
{
return Hash::check($password, $this->password);
}
/**
* Reset password
*/
public function resetPasswords()
{
return $this->hasMany(ResetPassword::class);
}
}

View File

@ -21,10 +21,9 @@ class ForgotPasswordNotification extends Notification
/**
* Create a new notification instance.
*/
public function __construct()
{
//
}
public function __construct(
public string $token
) {}
/**
* Obtener los canales de entrega de la notificación
@ -40,8 +39,11 @@ public function via(object $notifiable): array
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('auth.forgot-password.subject'))
->markdown('user.password-forgot');
->subject(__('auth.forgot.subject'))
->markdown('user.password-forgot', [
'user' => $notifiable,
'token' => $this->token
]);
}
/**

View File

@ -18,6 +18,6 @@ class DeleteResetPasswords
*/
public function __invoke()
{
ResetPassword::where('created_at', '<', Carbon::now()->subMinutes(10))->delete();
ResetPassword::where('created_at', '<', Carbon::now()->subMinutes(15))->delete();
}
}

View File

@ -4,10 +4,9 @@
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Validation\UnauthorizedException;
use Illuminate\Validation\ValidationException;
use Notsoweb\ApiResponse\Enums\ApiResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Notsoweb\LaravelCore\Http\APIException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
@ -32,21 +31,12 @@
]);
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
$exceptions->render(function (ServiceUnavailableHttpException $e, Request $request) {
if ($request->is('api/*')) {
return ApiResponse::NOT_FOUND->response();
}
});
$exceptions->render(function (UnauthorizedException $e, Request $request) {
if ($request->is('api/*')) {
return ApiResponse::UNAUTHORIZED->response();
}
});
$exceptions->render(function (ValidationException $e, Request $request) {
if ($request->is('api/*')) {
return ApiResponse::UNPROCESSABLE_CONTENT->response($e->errors());
return ApiResponse::SERVICE_UNAVAILABLE->response();
}
});
$exceptions->render(APIException::notFound(...));
$exceptions->render(APIException::unauthorized(...));
$exceptions->render(APIException::unprocessableContent(...));
})->create();

111
composer.lock generated
View File

@ -199,26 +199,26 @@
},
{
"name": "clue/redis-react",
"version": "v2.7.0",
"version": "v2.8.0",
"source": {
"type": "git",
"url": "https://github.com/clue/reactphp-redis.git",
"reference": "2283690f249e8d93342dd63b5285732d2654e077"
"reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/reactphp-redis/zipball/2283690f249e8d93342dd63b5285732d2654e077",
"reference": "2283690f249e8d93342dd63b5285732d2654e077",
"url": "https://api.github.com/repos/clue/reactphp-redis/zipball/84569198dfd5564977d2ae6a32de4beb5a24bdca",
"reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca",
"shasum": ""
},
"require": {
"clue/redis-protocol": "0.3.*",
"clue/redis-protocol": "^0.3.2",
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3",
"react/event-loop": "^1.2",
"react/promise": "^3 || ^2.0 || ^1.1",
"react/promise-timer": "^1.9",
"react/socket": "^1.12"
"react/promise": "^3.2 || ^2.0 || ^1.1",
"react/promise-timer": "^1.11",
"react/socket": "^1.16"
},
"require-dev": {
"clue/block-react": "^1.5",
@ -251,7 +251,7 @@
],
"support": {
"issues": "https://github.com/clue/reactphp-redis/issues",
"source": "https://github.com/clue/reactphp-redis/tree/v2.7.0"
"source": "https://github.com/clue/reactphp-redis/tree/v2.8.0"
},
"funding": [
{
@ -263,7 +263,7 @@
"type": "github"
}
],
"time": "2024-01-05T15:54:20+00:00"
"time": "2025-01-03T16:18:33+00:00"
},
{
"name": "defuse/php-encryption",
@ -1419,16 +1419,16 @@
},
{
"name": "laravel/framework",
"version": "v11.36.1",
"version": "v11.37.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "df06f5163f4550641fdf349ebc04916a61135a64"
"reference": "6cb103d2024b087eae207654b3f4b26646119ba5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/df06f5163f4550641fdf349ebc04916a61135a64",
"reference": "df06f5163f4550641fdf349ebc04916a61135a64",
"url": "https://api.github.com/repos/laravel/framework/zipball/6cb103d2024b087eae207654b3f4b26646119ba5",
"reference": "6cb103d2024b087eae207654b3f4b26646119ba5",
"shasum": ""
},
"require": {
@ -1478,7 +1478,6 @@
"voku/portable-ascii": "^2.0.2"
},
"conflict": {
"mockery/mockery": "1.6.8",
"tightenco/collect": "<5.5.33"
},
"provide": {
@ -1630,7 +1629,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-12-17T22:32:08+00:00"
"time": "2025-01-02T20:10:21+00:00"
},
{
"name": "laravel/passport",
@ -3386,16 +3385,16 @@
},
{
"name": "notsoweb/api-response",
"version": "1.0.2",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/notsoweb/api-response.git",
"reference": "037267a97d3c11f47a065b5043c2f0c68c9cc4fe"
"reference": "f90c565f8f7a976fd3df469256f0e4883d69a912"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/notsoweb/api-response/zipball/037267a97d3c11f47a065b5043c2f0c68c9cc4fe",
"reference": "037267a97d3c11f47a065b5043c2f0c68c9cc4fe",
"url": "https://api.github.com/repos/notsoweb/api-response/zipball/f90c565f8f7a976fd3df469256f0e4883d69a912",
"reference": "f90c565f8f7a976fd3df469256f0e4883d69a912",
"shasum": ""
},
"require": {
@ -3420,9 +3419,9 @@
"description": "Respuesta para APIs con el estándar Jsend para PHP",
"support": {
"issues": "https://github.com/notsoweb/api-response/issues",
"source": "https://github.com/notsoweb/api-response/tree/1.0.2"
"source": "https://github.com/notsoweb/api-response/tree/1.0.3"
},
"time": "2024-10-24T23:51:34+00:00"
"time": "2025-01-04T19:10:18+00:00"
},
{
"name": "notsoweb/laravel-core",
@ -3430,12 +3429,12 @@
"source": {
"type": "git",
"url": "https://github.com/notsoweb/laravel-core.git",
"reference": "12da28db3a1b4634616fefa4bfb9fc7c277b7393"
"reference": "ef35d982b6065419126961568766eb2b2b6c61f1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/notsoweb/laravel-core/zipball/12da28db3a1b4634616fefa4bfb9fc7c277b7393",
"reference": "12da28db3a1b4634616fefa4bfb9fc7c277b7393",
"url": "https://api.github.com/repos/notsoweb/laravel-core/zipball/ef35d982b6065419126961568766eb2b2b6c61f1",
"reference": "ef35d982b6065419126961568766eb2b2b6c61f1",
"shasum": ""
},
"require": {
@ -3467,7 +3466,7 @@
"issues": "https://github.com/notsoweb/laravel-core/issues",
"source": "https://github.com/notsoweb/laravel-core/tree/main"
},
"time": "2024-12-31T01:43:06+00:00"
"time": "2025-01-04T19:32:12+00:00"
},
{
"name": "nunomaduro/termwind",
@ -6006,16 +6005,16 @@
},
{
"name": "symfony/finder",
"version": "v7.2.0",
"version": "v7.2.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "6de263e5868b9a137602dd1e33e4d48bfae99c49"
"reference": "87a71856f2f56e4100373e92529eed3171695cfb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/6de263e5868b9a137602dd1e33e4d48bfae99c49",
"reference": "6de263e5868b9a137602dd1e33e4d48bfae99c49",
"url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb",
"reference": "87a71856f2f56e4100373e92529eed3171695cfb",
"shasum": ""
},
"require": {
@ -6050,7 +6049,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v7.2.0"
"source": "https://github.com/symfony/finder/tree/v7.2.2"
},
"funding": [
{
@ -6066,20 +6065,20 @@
"type": "tidelift"
}
],
"time": "2024-10-23T06:56:12+00:00"
"time": "2024-12-30T19:00:17+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v7.2.0",
"version": "v7.2.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "e88a66c3997859532bc2ddd6dd8f35aba2711744"
"reference": "62d1a43796ca3fea3f83a8470dfe63a4af3bc588"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/e88a66c3997859532bc2ddd6dd8f35aba2711744",
"reference": "e88a66c3997859532bc2ddd6dd8f35aba2711744",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/62d1a43796ca3fea3f83a8470dfe63a4af3bc588",
"reference": "62d1a43796ca3fea3f83a8470dfe63a4af3bc588",
"shasum": ""
},
"require": {
@ -6128,7 +6127,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.2.0"
"source": "https://github.com/symfony/http-foundation/tree/v7.2.2"
},
"funding": [
{
@ -6144,20 +6143,20 @@
"type": "tidelift"
}
],
"time": "2024-11-13T18:58:46+00:00"
"time": "2024-12-30T19:00:17+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v7.2.1",
"version": "v7.2.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "d8ae58eecae44c8e66833e76cc50a4ad3c002d97"
"reference": "3c432966bd8c7ec7429663105f5a02d7e75b4306"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/d8ae58eecae44c8e66833e76cc50a4ad3c002d97",
"reference": "d8ae58eecae44c8e66833e76cc50a4ad3c002d97",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/3c432966bd8c7ec7429663105f5a02d7e75b4306",
"reference": "3c432966bd8c7ec7429663105f5a02d7e75b4306",
"shasum": ""
},
"require": {
@ -6242,7 +6241,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.2.1"
"source": "https://github.com/symfony/http-kernel/tree/v7.2.2"
},
"funding": [
{
@ -6258,7 +6257,7 @@
"type": "tidelift"
}
],
"time": "2024-12-11T12:09:10+00:00"
"time": "2024-12-31T14:59:40+00:00"
},
{
"name": "symfony/mailer",
@ -7457,16 +7456,16 @@
},
{
"name": "symfony/translation",
"version": "v7.2.0",
"version": "v7.2.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5"
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/dc89e16b44048ceecc879054e5b7f38326ab6cc5",
"reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5",
"url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923",
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923",
"shasum": ""
},
"require": {
@ -7532,7 +7531,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.2.0"
"source": "https://github.com/symfony/translation/tree/v7.2.2"
},
"funding": [
{
@ -7548,7 +7547,7 @@
"type": "tidelift"
}
],
"time": "2024-11-12T20:47:56+00:00"
"time": "2024-12-07T08:18:10+00:00"
},
{
"name": "symfony/translation-contracts",
@ -7787,16 +7786,16 @@
},
{
"name": "tightenco/ziggy",
"version": "v2.4.1",
"version": "v2.4.2",
"source": {
"type": "git",
"url": "https://github.com/tighten/ziggy.git",
"reference": "8e002298678fd4d61155bb1d6e3837048235bff7"
"reference": "6612c8c9b2d5b3e74fd67c58c11465df1273f384"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tighten/ziggy/zipball/8e002298678fd4d61155bb1d6e3837048235bff7",
"reference": "8e002298678fd4d61155bb1d6e3837048235bff7",
"url": "https://api.github.com/repos/tighten/ziggy/zipball/6612c8c9b2d5b3e74fd67c58c11465df1273f384",
"reference": "6612c8c9b2d5b3e74fd67c58c11465df1273f384",
"shasum": ""
},
"require": {
@ -7851,9 +7850,9 @@
],
"support": {
"issues": "https://github.com/tighten/ziggy/issues",
"source": "https://github.com/tighten/ziggy/tree/v2.4.1"
"source": "https://github.com/tighten/ziggy/tree/v2.4.2"
},
"time": "2024-11-21T15:51:20+00:00"
"time": "2025-01-02T20:06:52+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",

View File

@ -55,7 +55,9 @@
|
*/
'url' => env('APP_URL', 'http://localhost'),
'url' => env('APP_URL', 'http://backend.localhost'),
'frontend_url' => env('APP_FRONTEND_URL', 'http://frontend.localhost'),
/*
|--------------------------------------------------------------------------

View File

@ -27,10 +27,12 @@ public function up(): void
$table->softDeletes();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
Schema::create('reset_passwords', function (Blueprint $table) {
$table->string('token');
$table->timestamp('created_at')->nullable();
$table->foreignId('user_id')
->constrained('users')
->cascadeOnDelete();
$table->timestamp('created_at');
});
Schema::create('sessions', function (Blueprint $table) {
@ -49,7 +51,7 @@ public function up(): void
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('reset_passwords');
Schema::dropIfExists('sessions');
}
};

View File

@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('reset_passwords', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('reset_passwords');
}
};

View File

@ -1,4 +1,6 @@
{
"Female": "Femenino",
"Male": "Masculino"
}
"Male": "Masculino",
"sincerely": "Atentamente",
"thanks": "Gracias"
}

View File

@ -18,7 +18,12 @@
'throttle' => 'Demasiados intentos de acceso. Por favor inténtelo de nuevo en :seconds segundos.',
'forgot' => [
'subject' => 'Recuperación de contraseña',
'line' => 'Por favor, haga clic en el siguiente enlace para restaurar su contraseña. El enlace expira en 15 minutos.',
'button' => 'Restaurar contraseña',
'description' => 'Por favor, haga clic en el siguiente enlace para restaurar su contraseña. El enlace expira en 15 minutos.',
'reset' => 'Restaurar contraseña',
],
'token' => [
'not_exists' => 'El token no existe.',
'expired' => 'El token caducado.',
'both' => 'El token no existe o ha caducado.',
]
];

View File

@ -1,10 +1,11 @@
<x-mail::message>
{{ __('auth.forgot.line') }}
{{ __('auth.forgot.description') }}
<x-mail::button :url="env('APP_FRONTEND_URL') . '/auth.html#/reset-password?code=12345234234'">
{{ __('auth.forgot.button') }}
<x-mail::button :url="config('app.frontend_url') . '/auth.html#/reset-password?token=' . $token . '&email=' . $user->email">
{{ __('auth.forgot.reset') }}
</x-mail::button>
{{ __('thanks')}},<br>
{{ config('app.name') }}
*{{ __('sincerely')}}*,<br>
{{ config('app.name') }}.
</x-mail::message>