feature: add error pages
@@ -8,6 +8,8 @@ use App\Services\Auth\AuthService;
|
|||||||
use App\Services\Auth\Contracts\AuthServiceInterface;
|
use App\Services\Auth\Contracts\AuthServiceInterface;
|
||||||
use App\Services\User\Contracts\UserServiceInterface;
|
use App\Services\User\Contracts\UserServiceInterface;
|
||||||
use App\Services\User\UserService;
|
use App\Services\User\UserService;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -21,6 +23,17 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
if (config('app.debug')) {
|
||||||
|
DB::listen(function ($query) {
|
||||||
|
Log::channel('query')->info(
|
||||||
|
sprintf(
|
||||||
|
"[%s ms] %s | Bindings: %s",
|
||||||
|
$query->time,
|
||||||
|
$query->sql,
|
||||||
|
json_encode($query->bindings)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ return [
|
|||||||
'path' => storage_path('logs/laravel.log'),
|
'path' => storage_path('logs/laravel.log'),
|
||||||
'level' => env('LOG_LEVEL', 'debug'),
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
'replace_placeholders' => true,
|
'replace_placeholders' => true,
|
||||||
|
'permission' => 0666,
|
||||||
],
|
],
|
||||||
|
|
||||||
'daily' => [
|
'daily' => [
|
||||||
@@ -71,6 +72,7 @@ return [
|
|||||||
'level' => env('LOG_LEVEL', 'debug'),
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
'days' => env('LOG_DAILY_DAYS', 14),
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
'replace_placeholders' => true,
|
'replace_placeholders' => true,
|
||||||
|
'permission' => 0666,
|
||||||
],
|
],
|
||||||
|
|
||||||
'slack' => [
|
'slack' => [
|
||||||
@@ -123,6 +125,13 @@ return [
|
|||||||
'handler' => NullHandler::class,
|
'handler' => NullHandler::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'query' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/query.log'),
|
||||||
|
'level' => 'debug',
|
||||||
|
'permission' => 0666,
|
||||||
|
],
|
||||||
|
|
||||||
'emergency' => [
|
'emergency' => [
|
||||||
'path' => storage_path('logs/laravel.log'),
|
'path' => storage_path('logs/laravel.log'),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ mkdir -p storage/logs \
|
|||||||
storage/framework/views \
|
storage/framework/views \
|
||||||
bootstrap/cache
|
bootstrap/cache
|
||||||
|
|
||||||
|
touch storage/logs/laravel.log storage/logs/query.log
|
||||||
|
chmod 666 storage/logs/laravel.log storage/logs/query.log
|
||||||
chmod -R 777 storage bootstrap/cache
|
chmod -R 777 storage bootstrap/cache
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# Tài liệu cấu hình và sử dụng System Error Pages
|
||||||
|
|
||||||
|
Tài liệu này hướng dẫn chi tiết cách hoạt động, các trường hợp sử dụng, cách kích hoạt (trigger) và kiểm thử các trang lỗi hệ thống trong dự án **thankcard-system**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Danh sách các trang lỗi & Tình huống sử dụng
|
||||||
|
|
||||||
|
| Status Code | Tên lỗi | Tình huống sử dụng | Key Localization (`errors.php`) |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **401** | Unauthorized | Người dùng chưa đăng nhập nhưng cố truy cập tài nguyên yêu cầu xác thực. | `errors.401_title` / `errors.401_message` |
|
||||||
|
| **403** | Forbidden | Người dùng đã đăng nhập nhưng không có quyền hạn thực hiện hành động này (ví dụ: User thường cố truy cập chức năng Admin). | `errors.403_title` / `errors.403_message` |
|
||||||
|
| **404** | Not Found | Đường dẫn (URL) không tồn tại, hoặc khi tìm dữ liệu bằng Model binding/Query không thấy mà muốn trả về lỗi. | `errors.404_title` / `errors.404_message` |
|
||||||
|
| **419** | Session Expired | Phiên làm việc hết hạn hoặc token CSRF không hợp lệ/hết hạn khi gửi Form (POST/PUT/DELETE). | `errors.419_title` / `errors.419_message` |
|
||||||
|
| **429** | Too Many Requests | Người dùng/IP gửi quá nhiều yêu cầu trong thời gian ngắn (vượt giới hạn Rate Limit của Middleware). | `errors.429_title` / `errors.429_message` |
|
||||||
|
| **500** | Internal Error | Lỗi xảy ra từ phía máy chủ hoặc do code PHP bị lỗi crash, kết nối cơ sở dữ liệu thất bại... | `errors.500_title` / `errors.500_message` |
|
||||||
|
| **503** | Service Unavailable | Hệ thống đang được bảo trì khi chạy lệnh `php artisan down`. | `errors.503_title` / `errors.503_message` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Cách kích hoạt (Trigger) trong Laravel
|
||||||
|
|
||||||
|
Trong Laravel, bạn có thể trigger các trang lỗi này bằng nhiều cách khác nhau tùy thuộc vào ngữ cảnh.
|
||||||
|
|
||||||
|
### 2.1. Sử dụng hàm `abort()` (Khuyên dùng trong Controller/Service)
|
||||||
|
Hàm `abort()` sẽ ném ra một `HttpException` và Laravel sẽ tự động bắt lấy để render trang lỗi tương ứng nằm trong `resources/views/errors/{status}.blade.php`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Ví dụ kiểm tra quyền trong Controller
|
||||||
|
if ($user->role !== 'admin') {
|
||||||
|
abort(403, 'Bạn không có quyền quản trị viên.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ví dụ không tìm thấy tài nguyên
|
||||||
|
$card = Card::find($id);
|
||||||
|
if (!$card) {
|
||||||
|
abort(404, 'Không tìm thấy thẻ cảm ơn.');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2. Sử dụng lệnh bảo trì hệ thống (Maintenance Mode)
|
||||||
|
Kích hoạt lỗi **503** bằng cách đưa ứng dụng vào trạng thái bảo trì:
|
||||||
|
```bash
|
||||||
|
php artisan down
|
||||||
|
```
|
||||||
|
Để tắt chế độ bảo trì và đưa hệ thống hoạt động bình thường:
|
||||||
|
```bash
|
||||||
|
php artisan up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3. Sử dụng Middleware (Rate Limiting cho lỗi 429)
|
||||||
|
Laravel tích hợp sẵn middleware `throttle`. Bạn có thể áp dụng nó trong file Routes (`routes/web.php` hoặc `routes/api.php`):
|
||||||
|
```php
|
||||||
|
Route::middleware('throttle:60,1')->group(function () {
|
||||||
|
Route::get('/send-card', [UserController::class, 'send']);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
*(Nếu một IP gọi quá 60 lần/phút, Laravel sẽ tự động ném ra lỗi 429 và render trang 429)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ví dụ triển khai thực tế
|
||||||
|
|
||||||
|
### Trong Controller/Service:
|
||||||
|
```php
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\User\Contracts\UserServiceInterface;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
protected $userService;
|
||||||
|
|
||||||
|
public function __construct(UserServiceInterface $userService)
|
||||||
|
{
|
||||||
|
$this->userService = $userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$user = $this->userService->findUserById($id);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
// Tự động render view errors/404.blade.php
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('user.profile', compact('user'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Checklist kiểm thử các trang lỗi
|
||||||
|
|
||||||
|
Để chắc chắn các trang lỗi hoạt động chính xác và hiển thị đẹp mắt, hãy tiến hành kiểm thử theo checklist sau:
|
||||||
|
|
||||||
|
- [ ] **Test 401 Unauthorized:**
|
||||||
|
- Tạm thời gỡ bỏ middleware auth ở một route và ném `abort(401)`.
|
||||||
|
- Kiểm tra xem có hiển thị nút **"Đi đến trang Đăng Nhập"** hay không.
|
||||||
|
- [ ] **Test 403 Forbidden:**
|
||||||
|
- Đăng nhập tài khoản User thường, sau đó truy cập một route dành riêng cho Admin hoặc ném `abort(403)`.
|
||||||
|
- Kiểm tra xem nút **"Quay lại Trang Chủ"** có hoạt động chính xác không.
|
||||||
|
- [ ] **Test 404 Not Found:**
|
||||||
|
- Truy cập một đường dẫn ngẫu nhiên không khai báo trong route (ví dụ: `http://localhost:8888/path-does-not-exist`).
|
||||||
|
- Kiểm tra giao diện và nút **"Quay lại Trang Chủ"**.
|
||||||
|
- [ ] **Test 419 Session Expired:**
|
||||||
|
- Mở một trang có form POST, xóa trường `_token` ẩn (CSRF) trong thẻ `<form>` bằng Inspect Element của trình duyệt rồi bấm Submit.
|
||||||
|
- Hoặc ném `abort(419)`.
|
||||||
|
- Kiểm tra xem có hiển thị nút **"Tải lại trang"** hay không.
|
||||||
|
- [ ] **Test 429 Too Many Requests:**
|
||||||
|
- Cấu hình tạm thời middleware `throttle:3,1` trên route chính và bấm F5 liên tục 4 lần.
|
||||||
|
- Hoặc gọi `abort(429)`.
|
||||||
|
- Kiểm tra thông báo lỗi.
|
||||||
|
- [ ] **Test 500 Internal Server Error:**
|
||||||
|
- Thêm một dòng code bị lỗi cú pháp cố ý trong Controller (ví dụ: gọi hàm không tồn tại `$this->nonExistentMethod()`).
|
||||||
|
- Hoặc gọi `abort(500)`.
|
||||||
|
- Đảm bảo giao diện trang 500 hiển thị thân thiện, không làm lộ stack trace kỹ thuật của code PHP.
|
||||||
|
- [ ] **Test 503 Maintenance:**
|
||||||
|
- Chạy lệnh `php artisan down` trong container Docker.
|
||||||
|
- Tải lại trình duyệt để xem trang bảo trì 503 và nút **"Thử lại"**.
|
||||||
|
- Chạy `php artisan up` để đưa website trở lại bình thường.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'401_title' => 'Chưa đăng nhập',
|
||||||
|
'401_message' => 'Bạn cần đăng nhập tài khoản trước khi<br>truy cập vào trang này.',
|
||||||
|
|
||||||
|
'403_title' => 'Không có quyền truy cập',
|
||||||
|
'403_message' => 'Bạn không được phân quyền để truy cập<br>vào tài nguyên hoặc hành động này.',
|
||||||
|
|
||||||
|
'404_title' => 'Không tìm thấy trang',
|
||||||
|
'404_message' => 'Trang bạn đang tìm kiếm không tồn tại hoặc<br>đã được di chuyển khỏi hệ thống.',
|
||||||
|
|
||||||
|
'419_title' => 'Phiên làm việc hết hạn',
|
||||||
|
'419_message' => 'Trang đã hết hạn bảo mật do lâu không tương tác.<br>Vui lòng tải lại trang và thử lại.',
|
||||||
|
|
||||||
|
'429_title' => 'Yêu cầu quá giới hạn',
|
||||||
|
'429_message' => 'Bạn đang gửi quá nhiều yêu cầu đến máy chủ.<br>Vui lòng đợi một lát rồi thử lại.',
|
||||||
|
|
||||||
|
'500_title' => 'Lỗi máy chủ nội bộ',
|
||||||
|
'500_message' => 'Hệ thống đã gặp sự cố đột xuất máy chủ.<br>Chúng tôi đang kiểm tra và khắc phục sớm nhất.',
|
||||||
|
|
||||||
|
'503_title' => 'Hệ thống đang bảo trì',
|
||||||
|
'503_message' => 'Hệ thống hiện đang được bảo trì nâng cấp định kỳ.<br>Xin vui lòng quay lại sau ít phút.',
|
||||||
|
|
||||||
|
'back_to_home' => 'Về trang chủ',
|
||||||
|
'back_to_login' => 'Đi đến Đăng Nhập',
|
||||||
|
'back' => 'Quay lại',
|
||||||
|
'retry' => 'Thử lại',
|
||||||
|
'reload' => 'Tải lại trang',
|
||||||
|
];
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="#1a4f8b">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 281 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="#1a4f8b">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 371 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="#1a4f8b">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" 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>
|
||||||
|
After Width: | Height: | Size: 313 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="#1a4f8b">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 224 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="#1a4f8b">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 207 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="#1a4f8b">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="#1a4f8b">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 780 B |
@@ -0,0 +1,25 @@
|
|||||||
|
@extends('errors.layout')
|
||||||
|
|
||||||
|
@section('title', __('errors.401_title'))
|
||||||
|
@section('code', '401')
|
||||||
|
|
||||||
|
@section('illustration')
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<img src="{{ asset('images/icn-401.svg') }}" alt="401 Illustration" class="h-20 sm:h-24 w-auto object-contain">
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('message')
|
||||||
|
{!! __('errors.401_message') !!}
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('action')
|
||||||
|
<button onclick="history.back();"
|
||||||
|
class="btn-secondary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-gray-100 active:scale-[0.97]">
|
||||||
|
{{ __('errors.back') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('login') }}"
|
||||||
|
class="btn-primary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-primary-hover active:scale-[0.97]">
|
||||||
|
{{ __('errors.back_to_login') }}
|
||||||
|
</a>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@extends('errors.layout')
|
||||||
|
|
||||||
|
@section('title', __('errors.403_title'))
|
||||||
|
@section('code', '403')
|
||||||
|
|
||||||
|
@section('illustration')
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<img src="{{ asset('images/icn-403.svg') }}" alt="403 Illustration" class="h-20 sm:h-24 w-auto object-contain">
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('message')
|
||||||
|
{!! __('errors.403_message') !!}
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('action')
|
||||||
|
<button onclick="history.back();"
|
||||||
|
class="btn-secondary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-gray-100 active:scale-[0.97]">
|
||||||
|
{{ __('errors.back') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ url('/') }}"
|
||||||
|
class="btn-primary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-primary-hover active:scale-[0.97]">
|
||||||
|
{{ __('errors.back_to_home') }}
|
||||||
|
</a>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@extends('errors.layout')
|
||||||
|
|
||||||
|
@section('title', __('errors.404_title'))
|
||||||
|
@section('code', '404')
|
||||||
|
|
||||||
|
@section('illustration')
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<img src="{{ asset('images/icn-404.svg') }}" alt="404 Illustration" class="h-20 sm:h-24 w-auto object-contain">
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('message')
|
||||||
|
{!! __('errors.404_message') !!}
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('action')
|
||||||
|
<button onclick="history.back();"
|
||||||
|
class="btn-secondary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-gray-100 active:scale-[0.97]">
|
||||||
|
{{ __('errors.back') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ url('/') }}"
|
||||||
|
class="btn-primary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-primary-hover active:scale-[0.97]">
|
||||||
|
{{ __('errors.back_to_home') }}
|
||||||
|
</a>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@extends('errors.layout')
|
||||||
|
|
||||||
|
@section('title', __('errors.419_title'))
|
||||||
|
@section('code', '419')
|
||||||
|
|
||||||
|
@section('illustration')
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<img src="{{ asset('images/icn-419.svg') }}" alt="419 Illustration" class="h-20 sm:h-24 w-auto object-contain">
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('message')
|
||||||
|
{!! __('errors.419_message') !!}
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('action')
|
||||||
|
<button onclick="window.location.reload();"
|
||||||
|
class="btn-primary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-primary-hover active:scale-[0.97]">
|
||||||
|
{{ __('errors.reload') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ url('/') }}"
|
||||||
|
class="btn-secondary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-gray-100 active:scale-[0.97]">
|
||||||
|
{{ __('errors.back_to_home') }}
|
||||||
|
</a>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@extends('errors.layout')
|
||||||
|
|
||||||
|
@section('title', __('errors.429_title'))
|
||||||
|
@section('code', '429')
|
||||||
|
|
||||||
|
@section('illustration')
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<img src="{{ asset('images/icn-429.svg') }}" alt="429 Illustration" class="h-20 sm:h-24 w-auto object-contain">
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('message')
|
||||||
|
{!! __('errors.429_message') !!}
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('action')
|
||||||
|
<button onclick="history.back();"
|
||||||
|
class="btn-secondary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-gray-100 active:scale-[0.97]">
|
||||||
|
{{ __('errors.back') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ url('/') }}"
|
||||||
|
class="btn-primary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-primary-hover active:scale-[0.97]">
|
||||||
|
{{ __('errors.back_to_home') }}
|
||||||
|
</a>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@extends('errors.layout')
|
||||||
|
|
||||||
|
@section('title', __('errors.500_title'))
|
||||||
|
@section('code', '500')
|
||||||
|
|
||||||
|
@section('illustration')
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<img src="{{ asset('images/icn-500.svg') }}" alt="500 Illustration" class="h-20 sm:h-24 w-auto object-contain">
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('message')
|
||||||
|
{!! __('errors.500_message') !!}
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('action')
|
||||||
|
<button onclick="history.back();"
|
||||||
|
class="btn-secondary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-gray-100 active:scale-[0.97]">
|
||||||
|
{{ __('errors.back') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ url('/') }}"
|
||||||
|
class="btn-primary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-primary-hover active:scale-[0.97]">
|
||||||
|
{{ __('errors.back_to_home') }}
|
||||||
|
</a>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@extends('errors.layout')
|
||||||
|
|
||||||
|
@section('title', __('errors.503_title'))
|
||||||
|
@section('code', '503')
|
||||||
|
|
||||||
|
@section('illustration')
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<img src="{{ asset('images/icn-503.svg') }}" alt="503 Illustration" class="h-20 sm:h-24 w-auto object-contain">
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('message')
|
||||||
|
{!! __('errors.503_message') !!}
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('action')
|
||||||
|
<button onclick="window.location.reload();"
|
||||||
|
class="btn-primary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-primary-hover active:scale-[0.97]">
|
||||||
|
{{ __('errors.retry') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ url('/') }}"
|
||||||
|
class="btn-secondary w-full sm:w-auto text-[13px] h-[40px] px-6 flex items-center justify-center transition-all duration-300 hover:scale-[1.03] hover:bg-gray-100 active:scale-[0.97]">
|
||||||
|
{{ __('errors.back_to_home') }}
|
||||||
|
</a>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>@yield('title') - GMO-Z.com RUNSYSTEM</title>
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="bg-[#f8fafc] text-text-dark font-sans antialiased min-h-screen flex items-center justify-center p-0 m-0">
|
||||||
|
<div class="w-full max-w-[480px] sm:w-[480px] bg-white border border-[#e2e8f0] p-6 sm:p-10 text-center transition-all duration-300 mx-4 my-auto sm:mx-0"
|
||||||
|
style="box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); border-radius: 24px;">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="mb-6 sm:mb-8 flex justify-center">
|
||||||
|
<img src="{{ asset('images/logo.png') }}" alt="GMO-Z.com RUNSYSTEM" class="h-8 sm:h-10 w-auto object-contain">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Illustration -->
|
||||||
|
@yield('illustration')
|
||||||
|
|
||||||
|
<!-- Error Code -->
|
||||||
|
<div class="mb-2 sm:mb-4">
|
||||||
|
<span class="text-[80px] sm:text-[110px] font-[900] text-primary leading-none tracking-tighter block select-none">
|
||||||
|
@yield('code')
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Title -->
|
||||||
|
<h1 class="text-[14px] sm:text-[16px] font-[700] text-text-dark mb-3 sm:mb-4 uppercase tracking-wider">
|
||||||
|
@yield('title')
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div class="text-[12px] sm:text-[13px] text-text-medium mb-6 sm:mb-8 leading-relaxed max-w-[360px] mx-auto">
|
||||||
|
@yield('message')
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex flex-col sm:flex-row justify-center items-center gap-3">
|
||||||
|
@yield('action')
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Copyright Footer -->
|
||||||
|
<div class="mt-8 sm:mt-12 pt-5 border-t border-[#f1f5f9]">
|
||||||
|
<span class="text-[10px] text-text-muted font-semibold">© 2026 GMO-Z.com RUNSYSTEM. All rights reserved.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,6 +9,17 @@ Route::get('/', function () {
|
|||||||
return redirect()->route('login');
|
return redirect()->route('login');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (config('app.debug')) {
|
||||||
|
Route::prefix('errors')->group(function () {
|
||||||
|
Route::get('{code}', function ($code) {
|
||||||
|
if (view()->exists("errors.{$code}")) {
|
||||||
|
return view("errors.{$code}");
|
||||||
|
}
|
||||||
|
abort(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Route::get('/test-modal', function() {
|
Route::get('/test-modal', function() {
|
||||||
return view('admin.users.index', [
|
return view('admin.users.index', [
|
||||||
'users' => [],
|
'users' => [],
|
||||||
|
|||||||