feature: update message for login feature

This commit is contained in:
antv
2026-07-06 21:02:52 +07:00
parent 7a964785c7
commit af529a4e81
12 changed files with 427 additions and 51 deletions
+38 -7
View File
@@ -14,7 +14,7 @@ class AuthController extends Controller
public function showLoginForm()
{
if (Auth::check()) {
if (Auth::check() && !session('dialog_first_login')) {
return redirect($this->authService->getRedirectRouteForUser(Auth::user()));
}
return view('login');
@@ -22,20 +22,51 @@ class AuthController extends Controller
public function login(Request $request)
{
$credentials = $request->validate([
// Custom validation check: treat all validation errors as "Invalid email/password"
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'mail' => 'required|email|max:255',
'password' => 'required|string|max:100',
]);
$result = $this->authService->attemptLogin($credentials, $request);
if (!$result['success']) {
if ($validator->fails()) {
return back()->withInput($request->only('mail'))->withErrors([
$result['error_key'] => $result['error_msg'],
'mail' => 'Thông tin đăng nhập không đúng. Vui lòng nhập lại',
]);
}
return redirect($this->authService->getRedirectRouteForUser($result['user']));
$credentials = $validator->validated();
$email = \Illuminate\Support\Str::lower($credentials['mail']);
$throttleKey = 'login-attempts:' . $email;
// Brute-force check (lock after 5 failed attempts)
if (\Illuminate\Support\Facades\RateLimiter::tooManyAttempts($throttleKey, 5)) {
return back()->withInput($request->only('mail'))->with('dialog_error_brute_force', 'Tài khoản của bạn tạm thời bị khóa do nhập sai mật khẩu quá 5 lần. Vui lòng thử lại sau 15 phút.');
}
$result = $this->authService->attemptLogin($credentials, $request);
if (!$result['success']) {
if ($result['error_key'] === 'permission') {
return back()->withInput($request->only('mail'))->with('dialog_error_permission', $result['error_msg']);
}
// For invalid password / email (counts towards brute-force attempts)
\Illuminate\Support\Facades\RateLimiter::hit($throttleKey, 900); // 15 minutes lockout
return back()->withInput($request->only('mail'))->withErrors([
'mail' => $result['error_msg'],
]);
}
// Clear rate limiter on success
\Illuminate\Support\Facades\RateLimiter::clear($throttleKey);
$user = $result['user'];
if ($user->first_login == \App\Models\User::FIRST_LOGIN_TRUE) {
return redirect()->route('login')->with('dialog_first_login', true);
}
return redirect($this->authService->getRedirectRouteForUser($user));
}
public function logout(Request $request)
+6 -2
View File
@@ -14,7 +14,7 @@ class AuthService implements AuthServiceInterface
// Manual MD5 password check
$user = User::where('mail', $credentials['mail'])->first();
if (!$user || $user->pass !== md5($credentials['password'])) {
return ['success' => false, 'error_key' => 'mail', 'error_msg' => __('auth.failed')];
return ['success' => false, 'error_key' => 'mail', 'error_msg' => 'Thông tin đăng nhập không đúng. Vui lòng nhập lại'];
}
// Log the user in manually
@@ -22,7 +22,7 @@ class AuthService implements AuthServiceInterface
$request->session()->regenerate();
if ($user->status != User::STATUS_ACTIVE) {
Auth::logout();
return ['success' => false, 'error_key' => 'mail', 'error_msg' => __('auth.inactive')];
return ['success' => false, 'error_key' => 'permission', 'error_msg' => 'Bạn không có quyền truy cập. Vui lòng liên hệ quản trị viên'];
}
return ['success' => true, 'user' => $user];
@@ -41,6 +41,10 @@ class AuthService implements AuthServiceInterface
return route('user.change_password');
}
if ($user->role == User::ROLE_ADMIN) {
return route('admin.dashboard');
}
return route('user.dashboard');
}
}
+6 -1
View File
@@ -25,5 +25,10 @@ return Application::configure(basePath: dirname(__DIR__))
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
$exceptions->render(function (\Illuminate\Auth\AuthenticationException $e, \Illuminate\Http\Request $request) {
if ($request->expectsJson()) {
return response()->json(['message' => $e->getMessage()], 401);
}
return redirect()->guest(route('login'))->with('session_expired', true);
});
})->create();
+3 -3
View File
@@ -19,9 +19,9 @@ The following reusable/common components are created and used across all display
| Case ID | Feature | Display Case | Notification Type | Message | Screenshot Path |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **01** | Authentication | Login failed | Toast (Error) | Đăng nhập thất bại. Vui lòng kiểm tra lại Email hoặc Mật khẩu. | [case_01_login_failed.png](case_01_login_failed.png) |
| **02** | Authorization | Access denied | Error Dialog | Tài khoản của bạn không có quyền truy cập vào trang quản trị này. Vui lòng liên hệ với quản trị viên nếu đây là một sự nhầm lẫn. | [case_02_access_denied.png](case_02_access_denied.png) |
| **03** | Authentication | First login (change password required) | Info Dialog | Chào mừng bạn đến với hệ thống Thankcard! Đây là lần đầu tiên bạn đăng nhập, vui lòng đổi mật khẩu mới để tiếp tục sử dụng hệ thống. | [case_03_first_login.png](case_03_first_login.png) |
| **01** | Authentication | Login failed | Inline (Error) | Thông tin đăng nhập không đúng. Vui lòng nhập lại | [case_01_login_failed.png](case_01_login_failed.png) |
| **02** | Authorization | Access denied / disabled | Error Dialog | Bạn không có quyền truy cập. Vui lòng liên hệ quản trị viên | [case_02_access_denied.png](case_02_access_denied.png) |
| **03** | Authentication | First login | Info Dialog | Vui lòng đổi mật khẩu trước khi tiếp tục sử dụng hệ thống. | [case_03_first_login.png](case_03_first_login.png) |
| **04** | User Management | Delete user confirmation | Confirm Dialog (Danger) | Bạn có chắc chắn muốn xóa tài khoản của nhân viên Nguyễn Văn A (MSNV: NV0001)? Hành động này không thể hoàn tác và tất cả dữ liệu liên quan sẽ bị xóa. | [case_04_delete_user_confirmation.png](case_04_delete_user_confirmation.png) |
| **05** | User Management | Create user success | Success Dialog | Tài khoản nhân viên mới đã được tạo thành công trong hệ thống. Email kích hoạt đã được gửi tới người dùng. | [case_05_create_user_success.png](case_05_create_user_success.png) |
| **06** | User Management | Email already exists | Error Dialog | Địa chỉ email 'nguyenvana@runsystem.net' đã được đăng ký bởi một nhân viên khác. Vui lòng sử dụng địa chỉ email khác. | [case_06_email_already_exists.png](case_06_email_already_exists.png) |
+2 -2
View File
@@ -9,6 +9,6 @@ return [
'forgot_password' => 'Quên mật khẩu?',
'system_name' => 'Hệ thống Thank Card',
'developed_by' => 'Được phát triển bởi GMO-Z.com RUNSYSTEM',
'failed' => 'Email hoặc mật khẩu không chính xác.',
'inactive' => 'Tài khoản của bạn đã bị khóa hoặc bạn đã nghỉ việc.',
'failed' => 'Thông tin đăng nhập không đúng. Vui lòng nhập lại',
'inactive' => 'Bạn không có quyền truy cập. Vui lòng liên hệ quản trị viên',
];
@@ -1,4 +1,4 @@
@props(['id', 'title' => 'Đã xảy ra lỗi', 'message' => '', 'buttonText' => 'Đóng'])
@props(['id', 'title' => 'Đã xảy ra lỗi', 'message' => '', 'buttonText' => 'Đóng', 'onclick' => null])
<div id="{{ $id }}" class="fixed inset-0 z-[100] hidden flex-col items-center justify-center outline-none opacity-0 transition-opacity duration-300" aria-labelledby="modal-title-{{ $id }}" role="dialog" aria-modal="true">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm cursor-pointer" onclick="closeModal('{{ $id }}')"></div>
@@ -14,7 +14,7 @@
<p class="text-slate-500 text-sm leading-relaxed">{{ $message }}</p>
</div>
<div class="px-6 py-4 bg-slate-50 flex justify-center border-t border-slate-100">
<button type="button" onclick="closeModal('{{ $id }}')" class="w-full sm:w-auto px-6 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
<button type="button" onclick="{{ $onclick ?? "closeModal('{$id}')" }}" class="w-full sm:w-auto px-6 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
{{ $buttonText }}
</button>
</div>
@@ -1,4 +1,4 @@
@props(['id', 'title' => 'Thông tin', 'message' => '', 'buttonText' => 'Đóng'])
@props(['id', 'title' => 'Thông tin', 'message' => '', 'buttonText' => 'Đóng', 'onclick' => null])
<div id="{{ $id }}" class="fixed inset-0 z-[100] hidden flex-col items-center justify-center outline-none opacity-0 transition-opacity duration-300" aria-labelledby="modal-title-{{ $id }}" role="dialog" aria-modal="true">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm cursor-pointer" onclick="closeModal('{{ $id }}')"></div>
@@ -14,7 +14,7 @@
<p class="text-slate-500 text-sm leading-relaxed">{{ $message }}</p>
</div>
<div class="px-6 py-4 bg-slate-50 flex justify-center border-t border-slate-100">
<button type="button" onclick="closeModal('{{ $id }}')" class="w-full sm:w-auto px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
<button type="button" onclick="{{ $onclick ?? "closeModal('{$id}')" }}" class="w-full sm:w-auto px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
{{ $buttonText }}
</button>
</div>
@@ -1,4 +1,4 @@
@props(['id', 'title' => 'Cảnh báo', 'message' => '', 'buttonText' => 'Đã hiểu'])
@props(['id', 'title' => 'Cảnh báo', 'message' => '', 'buttonText' => 'Đã hiểu', 'onclick' => null])
<div id="{{ $id }}" class="fixed inset-0 z-[100] hidden flex-col items-center justify-center outline-none opacity-0 transition-opacity duration-300" aria-labelledby="modal-title-{{ $id }}" role="dialog" aria-modal="true">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm cursor-pointer" onclick="closeModal('{{ $id }}')"></div>
@@ -14,7 +14,7 @@
<p class="text-slate-500 text-sm leading-relaxed">{{ $message }}</p>
</div>
<div class="px-6 py-4 bg-slate-50 flex justify-center border-t border-slate-100">
<button type="button" onclick="closeModal('{{ $id }}')" class="w-full sm:w-auto px-6 py-2 bg-amber-500 hover:bg-amber-600 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
<button type="button" onclick="{{ $onclick ?? "closeModal('{$id}')" }}" class="w-full sm:w-auto px-6 py-2 bg-amber-500 hover:bg-amber-600 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
{{ $buttonText }}
</button>
</div>
+87 -1
View File
@@ -65,7 +65,7 @@
<!-- Form -->
<div class="w-full max-w-[380px] mx-auto flex-1 relative z-10">
<form action="{{ route('login') }}" method="POST" class="space-y-5">
<form action="{{ route('login') }}" method="POST" class="space-y-5" novalidate>
@csrf
<div class="min-h-[40px] flex items-center justify-center transition-all duration-300" aria-live="polite">
@@ -180,5 +180,91 @@
</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 ý" />
@endif
@if(session('session_expired'))
<x-warning-dialog id="modal-session-expired" title="Phiên làm việc hết hạn" message="Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại." buttonText="OK" onclick="handleSessionExpiredOk()" />
@endif
@if(session('dialog_first_login'))
<x-info-dialog id="modal-first-login" title="Yêu cầu đổi mật khẩu" message="Vui lòng đổi mật khẩu trước khi tiếp tục sử dụng hệ thống." buttonText="OK" onclick="handleFirstLoginOk()" />
@endif
@if(session('dialog_error_brute_force'))
<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
<script>
window.openModal = function(id) {
const modal = document.getElementById(id);
if (!modal) return;
document.body.style.overflow = 'hidden';
modal.classList.remove('hidden');
modal.classList.add('flex');
requestAnimationFrame(() => {
modal.classList.remove('opacity-0');
modal.classList.add('opacity-100');
const panel = modal.querySelector('.modal-panel');
if (panel) {
panel.classList.remove('scale-95', 'translate-y-4');
panel.classList.add('scale-100', 'translate-y-0');
}
});
modal.setAttribute('tabindex', '-1');
modal.focus();
const escHandler = function(e) {
if (e.key === 'Escape') {
closeModal(id);
modal.removeEventListener('keydown', escHandler);
}
};
modal.addEventListener('keydown', escHandler);
};
window.closeModal = function(id) {
const modal = document.getElementById(id);
if (!modal) return;
modal.classList.remove('opacity-100');
modal.classList.add('opacity-0');
const panel = modal.querySelector('.modal-panel');
if (panel) {
panel.classList.remove('scale-100', 'translate-y-0');
panel.classList.add('scale-95', 'translate-y-4');
}
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
document.body.style.overflow = '';
}, 300);
};
window.handleSessionExpiredOk = function() {
closeModal('modal-session-expired');
window.location.href = "{{ route('login') }}";
};
window.handleFirstLoginOk = function() {
closeModal('modal-first-login');
window.location.href = "{{ route('user.change_password') }}";
};
document.addEventListener('DOMContentLoaded', () => {
@if(session('dialog_error_permission'))
openModal('modal-error-permission');
@endif
@if(session('session_expired'))
openModal('modal-session-expired');
@endif
@if(session('dialog_first_login'))
openModal('modal-first-login');
@endif
@if(session('dialog_error_brute_force'))
openModal('modal-error-brute-force');
@endif
});
</script>
</body>
</html>
@@ -261,13 +261,13 @@
<!-- All Case Modals/Toasts rendered as Reusable components -->
<!-- Case 1: Login failed -->
<x-toast id="toast-case-1" type="error" message="Đăng nhập thất bại. Vui lòng kiểm tra lại Email hoặc Mật khẩu." />
<x-toast id="toast-case-1" type="error" message="Thông tin đăng nhập không đúng. Vui lòng nhập lại" />
<!-- Case 2: Access denied -->
<x-error-dialog id="modal-case-2" title="Truy cập bị từ chối" message="Tài khoản của bạn không có quyền truy cập vào trang quản trị này. Vui lòng liên hệ với quản trị viên nếu đây là một sự nhầm lẫn." buttonText="Đồng ý" />
<x-error-dialog id="modal-case-2" title="Truy cập bị từ chối" message="Bạn không có quyền truy cập. Vui lòng liên hệ quản trị viên" buttonText="Đồng ý" />
<!-- Case 3: First login (change password required) -->
<x-info-dialog id="modal-case-3" title="Yêu cầu đổi mật khẩu" message="Chào mừng bạn đến với hệ thống Thankcard! Đây là lần đầu tiên bạn đăng nhập, vui lòng đổi mật khẩu mới để tiếp tục sử dụng hệ thống." buttonText="Đổi mật khẩu ngay" />
<x-info-dialog id="modal-case-3" title="Yêu cầu đổi mật khẩu" message="Vui lòng đổi mật khẩu trước khi tiếp tục sử dụng hệ thống." buttonText="OK" />
<!-- Case 4: Delete user confirmation -->
<x-confirm-dialog id="modal-case-4" title="Xóa người dùng" message="Bạn có chắc chắn muốn xóa tài khoản của nhân viên Nguyễn Văn A (MSNV: NV0001)? Hành động này không thể hoàn tác và tất cả dữ liệu liên quan sẽ bị xóa." confirmText="Xóa vĩnh viễn" cancelText="Hủy bỏ" type="danger" />
+32 -26
View File
@@ -2,54 +2,60 @@
@section('title', 'Đổi Mật Khẩu')
@section('content')
<div class="form-container mt-10">
<div class="form-header">
<h3 class="form-header-title">Đổi Mật Khẩu</h3>
<div class="max-w-[480px] mx-auto bg-white rounded-[24px] shadow-[0_10px_30px_rgba(15,23,42,0.05)] p-8 mt-6">
<div class="text-center mb-8">
<div class="w-12 h-12 bg-blue-50 text-[#3462f7] rounded-2xl flex items-center justify-center mx-auto mb-4">
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/>
</svg>
</div>
<h3 class="text-[20px] font-extrabold text-[#1a2b49]">Đổi mật khẩu</h3>
<p class="text-[13px] text-gray-500 font-medium mt-1">Cập nhật mật khẩu mới để bảo vệ tài khoản</p>
</div>
<form action="{{ route('user.update_password') }}" method="POST">
@csrf
<div class="form-body">
<div class="space-y-6">
@if(Auth::user()->first_login == \App\Models\User::FIRST_LOGIN_TRUE)
<div class="mb-6 p-4 bg-orange-50 border border-orange-200 text-orange-800 rounded-lg text-sm font-medium leading-relaxed flex items-start gap-2 shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 shrink-0 text-orange-500 mt-0.5">
<div class="p-4 bg-amber-50/70 border border-amber-100 text-amber-800 rounded-2xl text-xs font-medium leading-relaxed flex items-start gap-2 shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 shrink-0 text-amber-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Đây lần đầu tiên bạn đăng nhập vào hệ thống. Để đảm bảo an toàn, hệ thống yêu cầu bạn đổi mật khẩu trước khi sử dụng các chức ng.</span>
<span>Đây lần đầu tiên bạn đăng nhập. Vui lòng đổi mật khẩu để tiếp tục sử dụng hệ thống.</span>
</div>
@endif
<div class="form-group">
<label class="form-label" for="password">Mật khẩu mới <span class="text-red-500">*</span></label>
<div class="form-input-group">
<span class="form-input-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<div class="flex flex-col">
<label class="text-[13px] font-bold text-gray-700 mb-1.5" for="password">Mật khẩu mới <span class="text-red-500">*</span></label>
<div class="relative flex items-center">
<span class="absolute left-4 text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-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>
</span>
<input type="password" name="password" id="password" class="form-input form-input-with-icon" required placeholder="Nhập mật khẩu mới">
<input type="password" name="password" id="password" class="w-full h-[48px] pl-12 pr-4 bg-[#F8FAFC] border border-gray-200 rounded-xl outline-none text-[#1a2b49] text-[14px] placeholder-gray-400 focus:border-[#3462f7] focus:bg-white focus:ring-4 focus:ring-[#3462f7]/10 hover:border-gray-300 transition-all duration-200" required placeholder="Nhập mật khẩu mới">
</div>
@error('password')<span class="error-text">{{ $message }}</span>@enderror
@error('password')<span class="text-red-500 text-[12px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
</div>
<div class="form-group">
<label class="form-label" for="password_confirmation">Xác nhận lại mật khẩu mới <span class="text-red-500">*</span></label>
<div class="form-input-group">
<span class="form-input-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<div class="flex flex-col">
<label class="text-[13px] font-bold text-gray-700 mb-1.5" for="password_confirmation">Xác nhận lại mật khẩu mới <span class="text-red-500">*</span></label>
<div class="relative flex items-center">
<span class="absolute left-4 text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</span>
<input type="password" name="password_confirmation" id="password_confirmation" class="form-input form-input-with-icon" required placeholder="Nhập lại mật khẩu mới">
<input type="password" name="password_confirmation" id="password_confirmation" class="w-full h-[48px] pl-12 pr-4 bg-[#F8FAFC] border border-gray-200 rounded-xl outline-none text-[#1a2b49] text-[14px] placeholder-gray-400 focus:border-[#3462f7] focus:bg-white focus:ring-4 focus:ring-[#3462f7]/10 hover:border-gray-300 transition-all duration-200" required placeholder="Nhập lại mật khẩu mới">
</div>
</div>
</div>
<div class="form-footer">
@if(Auth::user()->first_login != \App\Models\User::FIRST_LOGIN_TRUE)
<a href="{{ route('user.dashboard') }}" class="btn-secondary">Hủy bỏ</a>
@endif
<button type="submit" class="btn-primary">Xác Nhận Đổi Mật Khẩu</button>
<div class="flex items-center gap-3 pt-4">
@if(Auth::user()->first_login != \App\Models\User::FIRST_LOGIN_TRUE)
<a href="{{ route('user.dashboard') }}" class="cursor-pointer flex-1 flex items-center justify-center h-[46px] rounded-xl border border-gray-200 bg-white hover:bg-gray-50 text-gray-600 text-[14px] font-bold shadow-sm transition-all duration-200">Hủy bỏ</a>
@endif
<button type="submit" class="cursor-pointer flex-1 flex items-center justify-center h-[46px] rounded-xl bg-[#3462f7] hover:bg-[#254edb] text-white text-[14px] font-bold shadow-md shadow-blue-500/10 transition-all duration-200">Xác nhận đổi mật khẩu</button>
</div>
</div>
</form>
</div>
+244
View File
@@ -0,0 +1,244 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\RateLimiter;
use Tests\TestCase;
class LoginNotificationTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
RateLimiter::clear('login-attempts:test@example.com');
}
/**
* Test Case 1 & 7: Invalid email/password and validation cases
*/
public function test_invalid_email_or_password_displays_inline_error(): void
{
// 1. Incorrect password
$user = User::create([
'msnv' => 5001,
'name' => 'Test User',
'mail' => 'test@example.com',
'pass' => md5('password123'),
'departments' => 1,
'role' => User::ROLE_MEMBER,
'status' => User::STATUS_ACTIVE,
'card' => 10,
'flag_send' => User::FLAG_SEND_ENABLED,
'first_login' => User::FIRST_LOGIN_FALSE,
]);
$response = $this->post('/login', [
'mail' => 'test@example.com',
'password' => 'wrong_password',
]);
$response->assertRedirect();
$response->assertSessionHasErrors(['mail' => 'Thông tin đăng nhập không đúng. Vui lòng nhập lại']);
// 2. Empty fields
$response2 = $this->post('/login', [
'mail' => '',
'password' => '',
]);
$response2->assertRedirect();
$response2->assertSessionHasErrors(['mail' => 'Thông tin đăng nhập không đúng. Vui lòng nhập lại']);
// 3. Invalid email format
$response3 = $this->post('/login', [
'mail' => 'invalid-email-format',
'password' => 'password123',
]);
$response3->assertRedirect();
$response3->assertSessionHasErrors(['mail' => 'Thông tin đăng nhập không đúng. Vui lòng nhập lại']);
// 4. Email too long
$response4 = $this->post('/login', [
'mail' => str_repeat('a', 256) . '@example.com',
'password' => 'password123',
]);
$response4->assertRedirect();
$response4->assertSessionHasErrors(['mail' => 'Thông tin đăng nhập không đúng. Vui lòng nhập lại']);
// 5. Password too long
$response5 = $this->post('/login', [
'mail' => 'test@example.com',
'password' => str_repeat('p', 101),
]);
$response5->assertRedirect();
$response5->assertSessionHasErrors(['mail' => 'Thông tin đăng nhập không đúng. Vui lòng nhập lại']);
}
/**
* Test Case 2: Account disabled / no access permission
*/
public function test_inactive_user_displays_permission_error_dialog(): void
{
$user = User::create([
'msnv' => 5002,
'name' => 'Inactive User',
'mail' => 'test@example.com',
'pass' => md5('password123'),
'departments' => 1,
'role' => User::ROLE_MEMBER,
'status' => User::STATUS_INACTIVE, // 0
'card' => 10,
'flag_send' => User::FLAG_SEND_ENABLED,
'first_login' => User::FIRST_LOGIN_FALSE,
]);
$response = $this->post('/login', [
'mail' => 'test@example.com',
'password' => 'password123',
]);
$response->assertRedirect();
$response->assertSessionHas('dialog_error_permission', 'Bạn không có quyền truy cập. Vui lòng liên hệ quản trị viên');
$this->assertFalse(\Illuminate\Support\Facades\Auth::check());
}
/**
* Test Case 3: Session expired
*/
public function test_session_expired_redirects_with_session_expired_flash(): void
{
$response = $this->get('/my-page'); // Requires auth
$response->assertRedirect(route('login'));
$response->assertSessionHas('session_expired', true);
}
/**
* Test Case 4: Login success
*/
public function test_login_success_redirects_correctly(): void
{
// 1. Member redirect to My Page
$member = User::create([
'msnv' => 5003,
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password123'),
'departments' => 1,
'role' => User::ROLE_MEMBER,
'status' => User::STATUS_ACTIVE,
'card' => 10,
'flag_send' => User::FLAG_SEND_ENABLED,
'first_login' => User::FIRST_LOGIN_FALSE,
]);
$response = $this->post('/login', [
'mail' => 'member@example.com',
'password' => 'password123',
]);
$response->assertRedirect(route('user.dashboard'));
$this->assertTrue(\Illuminate\Support\Facades\Auth::check());
$this->assertEquals($member->id, \Illuminate\Support\Facades\Auth::user()->id);
\Illuminate\Support\Facades\Auth::logout();
// 2. Admin redirect to Admin User List
$admin = User::create([
'msnv' => 5004,
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password123'),
'departments' => 1,
'role' => User::ROLE_ADMIN,
'status' => User::STATUS_ACTIVE,
'card' => 10,
'flag_send' => User::FLAG_SEND_ENABLED,
'first_login' => User::FIRST_LOGIN_FALSE,
]);
$response2 = $this->post('/login', [
'mail' => 'admin@example.com',
'password' => 'password123',
]);
$response2->assertRedirect(route('admin.dashboard'));
$this->assertTrue(\Illuminate\Support\Facades\Auth::check());
}
/**
* Test Case 5: First login
*/
public function test_first_login_redirects_to_login_page_with_first_login_dialog(): void
{
$user = User::create([
'msnv' => 5005,
'name' => 'First Login User',
'mail' => 'first@example.com',
'pass' => md5('password123'),
'departments' => 1,
'role' => User::ROLE_MEMBER,
'status' => User::STATUS_ACTIVE,
'card' => 10,
'flag_send' => User::FLAG_SEND_ENABLED,
'first_login' => User::FIRST_LOGIN_TRUE,
]);
$response = $this->post('/login', [
'mail' => 'first@example.com',
'password' => 'password123',
]);
$response->assertRedirect(route('login'));
$response->assertSessionHas('dialog_first_login', true);
$this->assertTrue(\Illuminate\Support\Facades\Auth::check());
}
/**
* Test Case 6: Brute-force protection
*/
public function test_brute_force_protection_locks_after_5_failed_attempts(): void
{
$user = User::create([
'msnv' => 5006,
'name' => 'Lockout User',
'mail' => 'test@example.com',
'pass' => md5('password123'),
'departments' => 1,
'role' => User::ROLE_MEMBER,
'status' => User::STATUS_ACTIVE,
'card' => 10,
'flag_send' => User::FLAG_SEND_ENABLED,
'first_login' => User::FIRST_LOGIN_FALSE,
]);
// First 5 attempts fail
for ($i = 0; $i < 5; $i++) {
$response = $this->post('/login', [
'mail' => 'test@example.com',
'password' => 'wrong_password',
]);
$response->assertSessionHasErrors(['mail' => 'Thông tin đăng nhập không đúng. Vui lòng nhập lại']);
}
// 6th attempt is blocked by brute force protection
$response6 = $this->post('/login', [
'mail' => 'test@example.com',
'password' => 'wrong_password',
]);
$response6->assertSessionHas('dialog_error_brute_force', 'Tài khoản của bạn tạm thời bị khóa do nhập sai mật khẩu quá 5 lần. Vui lòng thử lại sau 15 phút.');
// 7th attempt with CORRECT password is also blocked
$response7 = $this->post('/login', [
'mail' => 'test@example.com',
'password' => 'password123',
]);
$response7->assertSessionHas('dialog_error_brute_force', 'Tài khoản của bạn tạm thời bị khóa do nhập sai mật khẩu quá 5 lần. Vui lòng thử lại sau 15 phút.');
$this->assertFalse(\Illuminate\Support\Facades\Auth::check());
}
}