feat: forgot password

config: msg
This commit is contained in:
2026-07-30 22:42:38 +07:00
parent 781a0f8aa7
commit 36eec0a664
11 changed files with 284 additions and 4 deletions
+10 -2
View File
@@ -19,7 +19,15 @@ class ResetCardsCommand extends Command
public function handle()
{
$this->adminService->resetAllCards();
$this->info('Successfully reset all cards for active members to 0!');
try {
$this->adminService->resetAllCards();
$this->info('Successfully reset all cards for active members to 0!');
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::error('Lỗi xảy ra khi chạy command reset thẻ hàng tháng (cards:reset): ' . $e->getMessage());
$this->error('Reset cards failed: ' . $e->getMessage());
return Command::FAILURE;
}
return Command::SUCCESS;
}
}
+22
View File
@@ -74,4 +74,26 @@ class AuthController extends Controller
$this->authService->logout($request);
return redirect('/login');
}
public function forgotPassword(Request $request)
{
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'mail' => 'required|email|max:255',
], [
'mail.required' => config('constants.MSG_FORGOT_PWD_EMAIL_REQUIRED'),
'mail.email' => config('constants.MSG_FORGOT_PWD_EMAIL_INVALID'),
]);
if ($validator->fails()) {
return back()->withInput()->with('dialog_forgot_error', $validator->errors()->first());
}
$result = $this->authService->forgotPassword($request->input('mail'));
if (!$result['success']) {
return back()->withInput()->with('dialog_forgot_error', $result['error_msg']);
}
return back()->with('dialog_forgot_success', $result['message']);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ResetPasswordMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public User $user,
public string $newPassword
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: '[ThankCard System] Mật khẩu mới của bạn',
);
}
public function content(): Content
{
return new Content(
view: 'emails.reset_password',
);
}
public function attachments(): array
{
return [];
}
}
+4
View File
@@ -198,10 +198,14 @@ class AdminService implements AdminServiceInterface
public function resetAllCards(): void
{
$activeUserCount = User::where('status', config('constants.STATUS_ACTIVE'))->count();
User::where('status', config('constants.STATUS_ACTIVE'))->update([
'card' => 0,
'flag_send' => config('constants.FLAG_SEND_DISABLED'),
]);
\Illuminate\Support\Facades\Log::info("Thực hiện reset thẻ hàng tháng thành công cho {$activeUserCount} tài khoản active.");
}
public function updateAddCard(int $id, array $data): void
+32
View File
@@ -47,4 +47,36 @@ class AuthService implements AuthServiceInterface
return route('user.dashboard');
}
public function forgotPassword(string $email): array
{
$email = \Illuminate\Support\Str::lower(trim($email));
$user = User::where('mail', $email)->first();
if (!$user) {
return ['success' => false, 'error_msg' => config('constants.MSG_FORGOT_PWD_EMAIL_NOT_FOUND')];
}
if ($user->status != config('constants.STATUS_ACTIVE')) {
return ['success' => false, 'error_msg' => config('constants.MSG_FORGOT_PWD_ACCOUNT_INACTIVE')];
}
// Generate random password (8 characters)
$newPassword = \Illuminate\Support\Str::random(8);
// Update password with md5 and set first_login flag to 1
$user->pass = md5($newPassword);
$user->first_login = config('constants.FIRST_LOGIN_TRUE');
$user->save();
// Send Email
try {
\Illuminate\Support\Facades\Mail::to($user->mail)->send(new \App\Mail\ResetPasswordMail($user, $newPassword));
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to send reset password email: ' . $e->getMessage());
return ['success' => false, 'error_msg' => config('constants.MSG_FORGOT_PWD_MAIL_FAILED')];
}
return ['success' => true, 'message' => config('constants.MSG_FORGOT_PWD_SUCCESS')];
}
}
@@ -12,4 +12,6 @@ interface AuthServiceInterface
public function logout(Request $request): void;
public function getRedirectRouteForUser(User $user): string;
public function forgotPassword(string $email): array;
}
+7
View File
@@ -38,4 +38,11 @@ return [
'CHATOPS_API_URL' => env('CHATOPS_API_URL', 'http://10.1.12.160/aiapi/sent-message-user'),
'CHATOPS_API_TOKEN' => env('CHATOPS_API_TOKEN', 'ifOgk13pXIa6p2q6vvJB'),
'MSG_FORGOT_PWD_EMAIL_REQUIRED' => 'Vui lòng nhập địa chỉ email.',
'MSG_FORGOT_PWD_EMAIL_INVALID' => 'Địa chỉ email không đúng định dạng.',
'MSG_FORGOT_PWD_EMAIL_NOT_FOUND' => 'Email không tồn tại trong hệ thống. Vui lòng kiểm tra lại.',
'MSG_FORGOT_PWD_ACCOUNT_INACTIVE' => 'Tài khoản của bạn đã bị khóa hoặc ngừng hoạt động. Vui lòng liên hệ quản trị viên.',
'MSG_FORGOT_PWD_MAIL_FAILED' => 'Không thể gửi email lúc này. Vui lòng thử lại sau.',
'MSG_FORGOT_PWD_SUCCESS' => 'Mật khẩu mới đã được gửi tới email của bạn. Vui lòng kiểm tra hòm thư!',
];
+1 -1
View File
@@ -112,7 +112,7 @@ return [
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
'name' => env('MAIL_FROM_NAME', 'Thanks Card'),
],
];
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cấp mật khẩu mới - ThankCard System</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f0f4f9; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; -webkit-font-smoothing: antialiased;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed; background-color: #f0f4f9; padding: 40px 16px;">
<tr>
<td align="center">
<!-- Main Container Card -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px; background-color: #ffffff; border-radius: 24px; overflow: hidden; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.01); border: 1px solid #e2e8f0;">
<!-- Header with Branding -->
<tr>
<td align="center" style="padding: 40px 32px 24px 32px; background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%); border-bottom: 1px solid #f1f5f9;">
<!-- Logo / App Brand Header -->
<div style="font-size: 32px; font-weight: 800; color: #1e293b; letter-spacing: -0.5px; margin-bottom: 8px;">
Thanks <span style="color: #ff7eb3; font-family: 'Caveat', cursive, sans-serif; font-size: 38px;">card</span>
</div>
<div style="font-size: 13px; color: #64748b; font-weight: 500;">
GMO-Z.com RUNSYSTEM
</div>
</td>
</tr>
<!-- Body Content -->
<tr>
<td style="padding: 36px 32px;">
<!-- Welcome / Greeting Title -->
<div style="text-align: center; margin-bottom: 28px;">
<h1 style="margin: 0; font-size: 22px; font-weight: 700; color: #1e293b;">
Mật khẩu mới của bạn
</h1>
<p style="margin-top: 8px; font-size: 14px; color: #64748b; line-height: 1.6;">
Hệ thống đã nhận được yêu cầu khôi phục mật khẩu cho tài khoản của bạn.
</p>
</div>
<!-- Greeting text -->
<p style="font-size: 15px; color: #334155; line-height: 1.6; margin-bottom: 16px;">
Xin chào <strong>{{ $user->name }}</strong>,
</p>
<p style="font-size: 14px; color: #475569; line-height: 1.6; margin-bottom: 24px;">
Dưới đây mật khẩu ngẫu nhiên tạm thời được cấp cho tài khoản (<strong>{{ $user->mail }}</strong>):
</p>
<!-- Password Box Display -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-bottom: 28px;">
<tr>
<td align="center" style="background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); border: 2px dashed #3b82f6; border-radius: 16px; padding: 24px 16px;">
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; color: #2563eb; letter-spacing: 1px; margin-bottom: 8px;">
Mật khẩu mới ngẫu nhiên
</div>
<div style="font-size: 28px; font-weight: 800; color: #1d4ed8; letter-spacing: 4px; font-family: 'Courier New', Courier, monospace;">
{{ $newPassword }}
</div>
</td>
</tr>
</table>
<!-- Important Warning Note -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-bottom: 28px; background-color: #fff1f2; border-left: 4px solid #f43f5e; border-radius: 8px;">
<tr>
<td style="padding: 14px 16px;">
<div style="font-size: 13px; color: #be123c; font-weight: 600; line-height: 1.5;">
<strong>Lưu ý bảo mật:</strong> Để đảm bảo an toàn, hệ thống sẽ yêu cầu bạn <strong>đổi sang mật khẩu nhân mới</strong> ngay lần đăng nhập đầu tiên với mật khẩu tạm thời này.
</div>
</td>
</tr>
</table>
<!-- Action Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-bottom: 28px;">
<tr>
<td align="center">
<a href="{{ route('login') }}" target="_blank" style="display: inline-block; padding: 14px 32px; background-color: #1d4ed8; color: #ffffff; font-size: 14px; font-weight: 700; text-decoration: none; border-radius: 12px; box-shadow: 0 4px 12px rgba(29, 78, 216, 0.35);">
Đăng nhập ngay
</a>
</td>
</tr>
</table>
<p style="font-size: 13px; color: #94a3b8; line-height: 1.6; margin-bottom: 0;">
Nếu bạn không thực hiện yêu cầu này, vui lòng liên hệ ngay với Quản trị viên hệ thống để bảo vệ tài khoản.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td align="center" style="padding: 24px 32px; background-color: #f8fafc; border-top: 1px solid #f1f5f9; font-size: 12px; color: #94a3b8; line-height: 1.5;">
<div>&copy; {{ date('Y') }} GMO-Z.com RUNSYSTEM. All rights reserved.</div>
<div style="margin-top: 4px;">ThankCard System - Lan tỏa lời cảm ơn & kết nối đồng nghiệp.</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+59 -1
View File
@@ -89,7 +89,12 @@
<!-- Password Field -->
<div class="space-y-1.5">
<label for="password" class="block text-[13px] font-bold text-gray-700">Mật khẩu</label>
<div class="flex items-center justify-between">
<label for="password" class="block text-[13px] font-bold text-gray-700">Mật khẩu</label>
<button type="button" onclick="openModal('modal-forgot-password')" class="text-[12px] font-semibold text-[#2563eb] hover:text-[#1d4ed8] hover:underline focus:outline-none transition-colors">
Quên mật khẩu?
</button>
</div>
<div class="relative">
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0V10.5m-2.812 10.5h14.625c.621 0 1.125-.504 1.125-1.125V11.25c0-.621-.504-1.125-1.125-1.125H3.75c-.621 0-1.125.504-1.125 1.125v7.875c0 .621.504 1.125 1.125 1.125z" /></svg>
@@ -179,6 +184,45 @@
</div>
</div>
<!-- Modal Quên Mật Khẩu -->
<div id="modal-forgot-password" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 hidden items-center justify-center p-4 transition-opacity duration-300 opacity-0">
<div class="modal-panel bg-white rounded-2xl max-w-md w-full p-6 sm:p-8 shadow-2xl transform transition-all duration-300 scale-95 translate-y-4 relative">
<button type="button" onclick="closeModal('modal-forgot-password')" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 focus:outline-none p-1 rounded-lg">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
<div class="text-center mb-6">
<div class="w-12 h-12 bg-blue-50 text-[#2563eb] rounded-full flex items-center justify-center mx-auto mb-3">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>
</div>
<h3 class="text-xl font-bold text-gray-900">Quên mật khẩu?</h3>
<p class="text-xs text-gray-500 mt-1">Nhập địa chỉ email đăng tài khoản của bạn để nhận mật khẩu mới.</p>
</div>
<form action="{{ route('forgot_password') }}" method="POST" class="space-y-4">
@csrf
<div>
<label for="forgot-mail" class="block text-[13px] font-bold text-gray-700 mb-1">Email đăng </label>
<div class="relative">
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /></svg>
</span>
<input type="email" name="mail" id="forgot-mail" placeholder="nhanvien@gmo.runsystem.vn" required value="{{ old('mail') }}" class="w-full h-[46px] pl-[42px] pr-4 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:border-[#2563eb] focus:ring-1 focus:ring-[#2563eb] outline-none transition-all shadow-sm" />
</div>
</div>
<div class="pt-2 flex gap-3">
<button type="button" onclick="closeModal('modal-forgot-password')" class="w-1/2 h-[42px] bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-lg transition-all text-[13px]">
Hủy
</button>
<button type="submit" class="w-1/2 h-[42px] bg-[#1d4ed8] hover:bg-[#1e40af] text-white font-bold rounded-lg shadow-sm transition-all text-[13px]">
Gửi mật khẩu
</button>
</div>
</form>
</div>
</div>
<!-- Dialogs -->
@if(session('dialog_error_permission'))
<x-error-dialog id="modal-error-permission" title="Truy cập bị từ chối" message="{{ session('dialog_error_permission') }}" buttonText="Đồng ý" />
@@ -196,6 +240,14 @@
<x-error-dialog id="modal-error-brute-force" title="Tài khoản bị khóa" message="{{ session('dialog_error_brute_force') }}" buttonText="Đồng ý" />
@endif
@if(session('dialog_forgot_error'))
<x-error-dialog id="modal-forgot-error" title="Gửi yêu cầu thất bại" message="{{ session('dialog_forgot_error') }}" buttonText="Đóng" />
@endif
@if(session('dialog_forgot_success'))
<x-success-dialog id="modal-forgot-success" title="Thành công" message="{{ session('dialog_forgot_success') }}" buttonText="Đồng ý" />
@endif
<script>
window.openModal = function(id) {
const modal = document.getElementById(id);
@@ -263,6 +315,12 @@
@if(session('dialog_error_brute_force'))
openModal('modal-error-brute-force');
@endif
@if(session('dialog_forgot_error'))
openModal('modal-forgot-error');
@endif
@if(session('dialog_forgot_success'))
openModal('modal-forgot-success');
@endif
});
</script>
</body>
+1
View File
@@ -29,6 +29,7 @@ Route::get('/test-modal', function() {
});
Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::post('/forgot-password', [AuthController::class, 'forgotPassword'])->name('forgot_password');
Route::get('/notification-showcase', function () {
return view('notification-showcase');