diff --git a/app/Console/Commands/Broadcast.php b/app/Console/Commands/Broadcast.php index 5bc69ef..941f048 100644 --- a/app/Console/Commands/Broadcast.php +++ b/app/Console/Commands/Broadcast.php @@ -23,7 +23,7 @@ class Broadcast extends Command * * @var string */ - protected $description = 'Command description'; + protected $description = 'Servicio de broadcast'; /** * Execute the console command. diff --git a/app/Console/Commands/DownSecure.php b/app/Console/Commands/DownSecure.php index 0d8633a..4006a06 100644 --- a/app/Console/Commands/DownSecure.php +++ b/app/Console/Commands/DownSecure.php @@ -25,7 +25,7 @@ class DownSecure extends Command * * @var string */ - protected $signature = 'app:secure'; + protected $signature = 'down:secure'; /** * The console command description. diff --git a/app/Http/Controllers/System/LoginController.php b/app/Http/Controllers/System/LoginController.php index aa3928e..bee895d 100644 --- a/app/Http/Controllers/System/LoginController.php +++ b/app/Http/Controllers/System/LoginController.php @@ -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(); + } +} \ No newline at end of file diff --git a/app/Http/Requests/User/ResetPasswordRequest.php b/app/Http/Requests/User/ResetPasswordRequest.php new file mode 100644 index 0000000..22e1783 --- /dev/null +++ b/app/Http/Requests/User/ResetPasswordRequest.php @@ -0,0 +1,45 @@ + + * + * @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'), + ]; + } +} diff --git a/app/Models/ResetPassword.php b/app/Models/ResetPassword.php index 8f3c848..d7240ad 100644 --- a/app/Models/ResetPassword.php +++ b/app/Models/ResetPassword.php @@ -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); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 47b5a95..070758a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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); + } } diff --git a/app/Notifications/ForgotPasswordNotification.php b/app/Notifications/ForgotPasswordNotification.php index b1c7902..8d55549 100644 --- a/app/Notifications/ForgotPasswordNotification.php +++ b/app/Notifications/ForgotPasswordNotification.php @@ -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 + ]); } /** diff --git a/app/Schedules/DeleteResetPasswords.php b/app/Schedules/DeleteResetPasswords.php index d4fea7a..7a52340 100644 --- a/app/Schedules/DeleteResetPasswords.php +++ b/app/Schedules/DeleteResetPasswords.php @@ -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(); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 4e0b009..0554c7a 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -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(); diff --git a/composer.lock b/composer.lock index ed79d48..348f29b 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/app.php b/config/app.php index 3b31eaf..3352934 100644 --- a/config/app.php +++ b/config/app.php @@ -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'), /* |-------------------------------------------------------------------------- diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 4b79dcb..505c926 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -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'); } }; diff --git a/database/migrations/2024_12_12_205344_create_reset_passwords_table.php b/database/migrations/2024_12_12_205344_create_reset_passwords_table.php deleted file mode 100644 index 4e72ff8..0000000 --- a/database/migrations/2024_12_12_205344_create_reset_passwords_table.php +++ /dev/null @@ -1,28 +0,0 @@ -string('email')->index(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('reset_passwords'); - } -}; diff --git a/lang/es.json b/lang/es.json index a3ebdc4..672a44f 100644 --- a/lang/es.json +++ b/lang/es.json @@ -1,4 +1,6 @@ { "Female": "Femenino", - "Male": "Masculino" -} \ No newline at end of file + "Male": "Masculino", + "sincerely": "Atentamente", + "thanks": "Gracias" +} diff --git a/lang/es/auth.php b/lang/es/auth.php index 47f7cb1..8f4d4a5 100644 --- a/lang/es/auth.php +++ b/lang/es/auth.php @@ -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.', + ] ]; diff --git a/resources/views/user/password-forgot.blade.php b/resources/views/user/password-forgot.blade.php index 1aa40c0..2c0ab1f 100644 --- a/resources/views/user/password-forgot.blade.php +++ b/resources/views/user/password-forgot.blade.php @@ -1,10 +1,11 @@ -{{ __('auth.forgot.line') }} +{{ __('auth.forgot.description') }} - -{{ __('auth.forgot.button') }} + +{{ __('auth.forgot.reset') }} -{{ __('thanks')}},
-{{ config('app.name') }} +*{{ __('sincerely')}}*,
+{{ config('app.name') }}. +