feature: add error pages

This commit is contained in:
antv
2026-06-30 15:31:40 +07:00
parent 96eac88f36
commit 8cbdd93e56
21 changed files with 435 additions and 1 deletions
+14 -1
View File
@@ -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)
)
);
});
}
} }
} }
+9
View File
@@ -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'),
], ],
+2
View File
@@ -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 "$@"
+124
View File
@@ -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.
+30
View File
@@ -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',
];
+3
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+4
View File
@@ -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

+25
View File
@@ -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
+25
View File
@@ -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
+25
View File
@@ -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
+25
View File
@@ -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
+25
View File
@@ -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
+25
View File
@@ -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
+25
View File
@@ -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
+48
View File
@@ -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">&copy; 2026 GMO-Z.com RUNSYSTEM. All rights reserved.</span>
</div>
</div>
</body>
</html>
+11
View File
@@ -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' => [],