Hi, xin chào mọi người
Đây là cách làm của mình, mọi người có thể tham khảo nha.
Setting .env
Setting .env và bật bảo mật 2 lớp cho gmail và tạo mật khẩu ứng dụng cho gmail, mật khẩu ứng dụng 12 ký tự này chính là password_email
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=address_email
MAIL_PASSWORD=password_email
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=
Thêm 2 trường confirm_otp, token_forgot
Ở đây mình viết thêm file migration để add thêm 2 column mới cho table users, sau đó chạy php artisan migrate
<?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::table('users', function($table) { $table->string('confirm_otp')->default(NULL); $table->string('token_forgot')->default(NULL); }); } /** * Reverse the migrations. */ public function down(): void { Schema::table('users', function($table) { $table->dropColumn('confirm_otp'); $table->dropColumn('token_forgot'); }); }
};
Thêm route
Route::get('/forgot-password', [ForgotPasswordController::class, 'index'])->name('forgot-password');
Route::post('/forgot-password', [ForgotPasswordController::class, 'forgot_password'])->name('forgot-password');
Route::get('/forgot_otp/{token}', [ForgotPasswordController::class, 'forgot_otp_view'])->name('forgot_otp_view');
Route::post('/forgot_otp', [ForgotPasswordController::class, 'forgot_otp'])->name('forgot_otp');
Route::post('/re_forgot', [ForgotPasswordController::class, 're_forgot'])->name('re_forgot');
Mọi người tự thiết kế 2 màn:
Màn 1: Để nhập email
Đoạn call ajax khi nhập email, mọi người có thể tham khảo
$('#submit-forgot').click(function(e) { e.preventDefault(); var formData = new FormData($('#formAuthentication')[0]); $.ajax({ type: "POST", url: "{{ route('forgot-password') }}", data: formData, cache: false, contentType: false, processData: false, success: function(data) { let path = document.querySelector('html').getAttribute('data-path') window.location.href = path + 'forgot_otp/' + data.token; }, error: function(a) { $('.message-error').remove(); let error = a['responseJSON'].message; if (error) { $('#email').parent().append('<div class="alert alert-danger message-error" role="alert">' + error + '</div>'); } else { $('#email').parent().append('<div class="alert alert-danger message-error" role="alert">' + a['responseJSON'].email + '</div>'); } } }); });
Màn 2: Để nhập OTP
Đoạn call ajax nhập OTP
$('#submit-otp').click(function(e) { e.preventDefault(); var formData = new FormData($('#twoStepsForm')[0]); $.ajax({ type: "POST", url: "{{ route('forgot_otp') }}", data: formData, cache: false, contentType: false, processData: false, success: function(data) { toastr.success(data.message); let path = document.querySelector('html').getAttribute('data-path'); setTimeout(() => { window.location.href = path + 'login'; // đoạn này mình cho sau 2 giây mới redirect để user còn nhìn thấy alert success }, 2000); }, error: function(a) { let error = a['responseJSON'].message; $('.message-error').remove(); $('#twoStepsForm .fv-plugins-icon-container').addClass( 'fv-plugins-bootstrap5-row-invalid'); if (error) { $('#otp').parent().append( '<div class="fv-plugins-message-container invalid-feedback message-error"><div data-field="otp" data-validator="notEmpty">' + error + '</div></div>'); toastr.error(error); } else { $('#otp').parent().append( '<div class="fv-plugins-message-container invalid-feedback message-error"><div data-field="otp" data-validator="notEmpty">' + a['responseJSON'].otp + '</div></div>'); // show message, mn có thể tùy chỉnh toastr.error(a['responseJSON'].otp); // Show alert error } } }); }); $('#re-forgot').click(function(e) { e.preventDefault(); var formData = new FormData(); formData.append('token', "{{ $token }}"); formData.append('_token', "{{ csrf_token() }}"); $.ajax({ type: "POST", url: '/re_forgot', data: formData, cache: false, contentType: false, processData: false, success: function(data) { toastr.success("OTP has been sent."); // ở đây mình dùng toastr để show alert message }, error: function(a) { // toastr.error(a['responseJSON'].message); } }); });
Gửi mail
Tạo 2 mẫu cho mail, 1 cho gửi OTP, 1 cho gửi newpassword
php artisan make:mail UserForGot
php artisan make:mail UserNewPassword
<?php
namespace App\Mail; use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; class UserForGot extends Mailable
{ use Queueable, SerializesModels; /** * Create a new message instance. */ protected $user; public function __construct(User $user) { $this->user = $user; } public function build() { return $this->view('mail.verify_forgot')->with(['user' => $this->user, 'token' => $this->user->token_forgot]); }
}
<?php
namespace App\Mail; use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; class UserNewPassword extends Mailable
{ use Queueable, SerializesModels; /** * Create a new message instance. */ protected $user; public function __construct(User $user) { $this->user = $user; } public function build() { return $this->view('mail.new_password')->with(['user' => $this->user, 'password' => $this->user->password_new]); }
}
Tạo 2 template gửi mail
Chỗ này mn tùy chỉnh miễn sao show ra được otp ra: $user->confirm_otp
- Màn content email verify_forgot.blade.php
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8" /> <title></title> <style> body { margin: 0; padding: 0; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: #333; background-color: #fff; } .container { margin: 0 auto; width: 100%; max-width: 600px; padding: 0 0px; padding-bottom: 10px; border-radius: 5px; line-height: 1.8; } .header { border-bottom: 1px solid #eee; } .header a { font-size: 1.4em; color: #000; text-decoration: none; font-weight: 600; } .content { min-width: 700px; overflow: auto; line-height: 2; } .otp { background: linear-gradient(to right, #00bc69 0, #00bc88 50%, #00bca8 100%); margin: 0 auto; width: max-content; padding: 0 10px; color: black; border-radius: 4px; text-shadow: 2px 0 #fff, -2px 0 #fff, 0 2px #fff, 0 -2px #fff, 1px 1px #fff, -1px -1px #fff, 1px -1px #fff, -1px 1px #fff; } .footer { color: #aaa; font-size: 0.8em; line-height: 1; font-weight: 300; } .email-info { color: #666666; font-weight: 400; font-size: 13px; line-height: 18px; padding-bottom: 6px; } .email-info a { text-decoration: none; color: #00bc69; } </style>
</head> <body> <div class="container"> <div class="header"> <a>Prove Your [company name] Identity</a> </div> <br /> <strong>Dear {{ $user->last_name }},</strong> <p> We have received your [company name] account registration request. <br /> <b>Your OTP code is:</b> </p> <h2 class="otp">{{ $user->confirm_otp }}</h2> <h5>Click on the link below to enter OTP:</h5> <a href="{{ route('forgot_otp_view', $token) }}">{{ route('forgot_otp_view', $token) }}</a> <p style="font-size: 0.9em"> <br /> <br /> Please ensure the confidentiality of your OTP and do not share it with anyone.<br /> <strong>Do not forward or give this code to anyone.</strong> <br /> <br /> <strong>Thank you for using [company name].</strong> <br /> <br /> Best regards, <br /> <strong>[company name]</strong> </p> <hr style="border: none; border-top: 0.5px solid #131111" /> <div class="footer"> <p>This email can't receive replies.</p> <p> For more information about [app name] and your account, visit <strong><a href="{{ route('login') }}" alt="companyABC">[app name]</a></strong> // Công ty ABC </p> </div> </div> <div style="text-align: center"> <div class="email-info"> © 2024 [company]. All rights reserved. </div> </div>
</body>
</html>
- Màn content email new_password.blade.php
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8" /> <title></title> <style> body { margin: 0; padding: 0; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: #333; background-color: #fff; } .container { margin: 0 auto; width: 100%; max-width: 600px; padding: 0 0px; padding-bottom: 10px; border-radius: 5px; line-height: 1.8; } .header { border-bottom: 1px solid #eee; } .header a { font-size: 1.4em; color: #000; text-decoration: none; font-weight: 600; } .content { min-width: 700px; overflow: auto; line-height: 2; } .otp { background: linear-gradient(to right, #00bc69 0, #00bc88 50%, #00bca8 100%); margin: 0 auto; width: max-content; padding: 0 10px; color: black; border-radius: 4px; text-shadow: 2px 0 #fff, -2px 0 #fff, 0 2px #fff, 0 -2px #fff, 1px 1px #fff, -1px -1px #fff, 1px -1px #fff, -1px 1px #fff; } .footer { color: #aaa; font-size: 0.8em; line-height: 1; font-weight: 300; } .email-info { color: #666666; font-weight: 400; font-size: 13px; line-height: 18px; padding-bottom: 6px; } .email-info a { text-decoration: none; color: #00bc69; } </style>
</head> <body> <div class="container"> <div class="header"> <a>YOUR NEW PASSWORD</a> </div> <br /> <strong>Dear {{ $user->last_name }},</strong> <p> OTP confirmation successful. <br /> <b>Your Password is:</b> </p> <h2 class="otp">{{ $password }}</h2> <p style="font-size: 0.9em"> <br /> <br /> <a href="{{ route('login') }}" alt="[app name]">LOGIN NOW</a> <br /> <br /> <strong>Thank you for using [app name].</strong> <br /> <br /> Best regards, <br /> <strong>[company name]</strong> </p> <hr style="border: none; border-top: 0.5px solid #131111" /> <div class="footer"> <p>This email can't receive replies.</p> <p> For more information about [app name] and your account, visit <strong><a href="{{ route('login') }}" alt="[company name]">[App name]</a></strong> </p> </div> </div> <div style="text-align: center"> <div class="email-info"> © 2024 [company name]. All rights reserved. </div> </div>
</body>
</html>
ForgotPasswordController
Trong đây mình có thêm điều kiện với những accoutn chưa verify khi đăng kí (kiểu như chưa nhập OTP nên mình có check thêm tại đây, mn có thể bỏ mấy đoạn logic ko cần thiết đi cho phù hợp nha)
<?php namespace App\Http\Controllers\Client\Auth; use App\Http\Controllers\Controller;
use App\Mail\UserForGot;
use App\Mail\UserNewPassword;
use App\Mail\UserVerification;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str; class ForgotPasswordController extends Controller
{ public function index() { return view('auth.client.forgot-password'); } public function forgot_password(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|string|email|max:100', ]); if ($validator->fails()) { return response()->json($validator->errors(), 400); } $user = User::where('email', $request->email)->first(); if (!$user || $user->is_verify != User::VERIFY) { return response()->json(['message' => 'Account not registered!'], 400); } try { DB::beginTransaction(); $user->confirm_otp = rand(100000, 999999); // $user->confirmation_code_expired_in = Carbon::now()->addSecond(60); $token = Str::random(64); $user->token_forgot = $token; $user->save(); Mail::to($user->email)->send(new UserForGot($user)); DB::commit(); return response()->json(['token' => $token], 200); } catch (\Throwable $exeption) { return $this->sendError(null, 400, $exeption->getMessage()); DB::rollBack(); } // return redirect()->route('forgot_otp_view', $token); } public function forgot_otp_view($token) { return view('auth.client.forgot-otp', compact('token')); } public function forgot_otp(Request $request) { $validator = Validator::make($request->all(), [ 'token' => 'required|string', 'otp' => 'required|string', ]); if ($validator->fails()) { return response()->json($validator->errors(), 400); } $users = User::where('is_verify', User::VERIFY)->get(); try { DB::beginTransaction(); foreach ($users as $user) { if ($user->token_forgot == $request->token) { if ($user->confirm_otp == $request->otp) { $password = Str::random(8); $user->password = Hash::make($password); $user->token_forgot = ''; $user->save(); $user->password_new = $password; Mail::to($user->email)->send(new UserNewPassword($user)); break; } else { return response()->json(['status' => false, 'message' => 'Your OTP is invalid'], 400); } } } DB::commit(); return response()->json(['status' => true, 'message' => 'Password has been sent to your inbox.'], 200); } catch (\Throwable $exeption) { DB::rollBack(); return $this->sendError(null, 400, $exeption->getMessage()); } } public function re_forgot(Request $request) { $validator = Validator::make($request->all(), [ 'token' => 'required|string|max:100', ]); if ($validator->fails()) { return response()->json($validator->errors(), 400); } // $user = User::where('email', $request->email)->first(); $users = User::where('is_verify', User::VERIFY)->get(); try { DB::beginTransaction(); foreach ($users as $user) { if (Hash::check($user->email, $request->token)) { if ($user['is_verify'] == User::VERIFY) { $user->confirm_otp = rand(100000, 999999); // $user->confirmation_code_expired_in = Carbon::now()->addSecond(60); $user->token_forgot = $request->token; $user->save(); Mail::to($user->email)->send(new UserForGot($user)); break; } else { return response()->json(['status' => false, 'message' => 'Account not registered!'], 400); } } } DB::commit(); return response()->json(['status' => true, 'message' => 'OTP has been sent to your inbox.'], 200); } catch (\Throwable $exeption) { DB::rollBack(); return $this->sendError(null, 400, $exeption->getMessage()); } }
}
Bổ sung 1 chút, trong class Controller mình có thêm 2 function này
<?php namespace App\Http\Controllers; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; class Controller extends BaseController
{ use AuthorizesRequests, ValidatesRequests; public function sendSuccess($data, $code = 200, $message = '') { return response()->json([ 'data' => $data, 'success' => true, 'message' => $message, ], $code); } public function sendError($data = null, $code = 400, $message = '') { return response()->json([ 'data' => $data, 'success' => false, 'message' => $message, ], $code); }
}
Cảm ơn mn đã dành thời gian đọc hết bài. Chúc mọi người thành công