Compare commits

..

14 Commits

Author SHA1 Message Date
debesocial 9b17d91380 docs: update internal documentation hub with granular permissions and new 2FA features 2026-05-21 22:26:01 +07:00
debesocial 7965b34c85 security: expand and complete permissions matrix with granular, enterprise-ready permissions 2026-05-21 22:15:53 +07:00
debesocial 65804be1cb security: secure role, notification, system setting, and documentation pages with spatie permissions 2026-05-21 22:10:36 +07:00
debesocial bf42ca956d security: enforce global 2FA toggles on login challenges and controller endpoints 2026-05-21 22:06:26 +07:00
debesocial 1a30122c3d feat: add global toggles for TOTP and Email 2FA in system settings, conditionally show/hide user 2FA tab 2026-05-21 22:05:14 +07:00
debesocial 6c582282ac feat: live-verify SMTP configuration by sending verification test mail before enabling Email 2FA 2026-05-21 21:58:26 +07:00
debesocial 41bef637c9 feat: enforce SMTP configuration validation and show warning notices before enabling Email 2FA 2026-05-21 21:55:00 +07:00
debesocial 4741a2dff2 feat: implement mutual exclusion and warnings between Google Authenticator and Email 2FA 2026-05-21 21:48:12 +07:00
debesocial 0d083765ff feat: implement premium Email 2FA authentication integrated with auth flow 2026-05-21 21:46:53 +07:00
debesocial a0673129ee feat: complete install.sh and run.sh scripts with dynamic health check and build assets sync 2026-05-21 21:38:28 +07:00
debesocial 177160ef03 feat: add unified install.sh and enhance self-healing in run.sh 2026-05-21 21:29:38 +07:00
debesocial e024777a72 docs: update and complete README with advanced features, tree structure, and orchestration details 2026-05-21 16:53:46 +07:00
debesocial 96d5d8717f docs: expand README with directory structures, tech stack specs and detailed developer guides 2026-05-21 16:53:16 +07:00
debesocial 7be12c03aa docs: update README for biiproject-kit v2 2026-05-21 16:52:47 +07:00
67 changed files with 1727 additions and 666 deletions
+52
View File
@@ -0,0 +1,52 @@
APP_NAME="Biiproject Kit"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost:8000
APP_PORT=8000
VITE_PORT=5173
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=pgsql
DB_HOST=pgsql
DB_PORT=5432
DB_DATABASE=biiskit
DB_USERNAME=sail
DB_PASSWORD=password
FORWARD_DB_PORT=5432
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=public
QUEUE_CONNECTION=database
CACHE_STORE=redis
CACHE_PREFIX=bii_kit_
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
FORWARD_REDIS_PORT=6379
MAIL_MAILER=log
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@biiproject.com"
MAIL_FROM_NAME="${APP_NAME}"
# OAuth (Passport)
PASSPORT_PERSONAL_ACCESS_CLIENT_ID=
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=
+202 -85
View File
@@ -1,44 +1,106 @@
# biiproject kit # biiproject-kit v2
A production-ready Laravel + Inertia.js starter kit with full RBAC, API auth, activity logging, and system settings — built to ship fast. [![Laravel](https://img.shields.io/badge/Laravel-11.x-FF2D20?style=for-the-badge&logo=laravel)](https://laravel.com)
[![React](https://img.shields.io/badge/React-18.x-61DAFB?style=for-the-badge&logo=react)](https://react.dev)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?style=for-the-badge&logo=typescript)](https://www.typescriptlang.org)
[![TailwindCSS](https://img.shields.io/badge/TailwindCSS-v4.x-06B6D4?style=for-the-badge&logo=tailwindcss)](https://tailwindcss.com)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?style=for-the-badge&logo=docker)](https://www.docker.com)
## Stack A high-performance, enterprise-grade **Laravel 11 + Inertia.js v2 + React 18** starter kit designed to accelerate the shipping times of SaaS and corporate applications. **Version 2** introduces advanced features such as robust multi-factor authentication (2FA), customized application branding, full system auditing, and ready-to-use OAuth2 integration.
| Layer | Technology | ---
|---|---|
| Backend | Laravel 13, PHP 8.3, PostgreSQL |
| Frontend | React 18, TypeScript, TailwindCSS v4, Vite 8 |
| Bridge | Inertia.js v2 |
| Auth | Breeze (web session) + Sanctum (API token) + Passport (OAuth2/SSO) |
| RBAC | spatie/laravel-permission |
| Logging | spatie/laravel-activitylog |
| Export/Import | maatwebsite/excel |
| API Docs | knuckleswtf/scribe |
## Quick Start ## 🚀 Key Architectural Improvements in v2
This project is fully containerized and features an automated startup script. * 🔒 **Granular Security Gateways** — Integrated Time-based One-time Password (TOTP) compatible with Google Authenticator, Authy, or 1Password. Full dynamic login challenge flow with fallback recovery codes.
* 🛡️ **Advanced Spatie RBAC Matrix** — Sleek dashboard (`/roles`) allowing real-time permission modifications per role without code adjustments.
* ⚙️ **Dynamic Brand & Settings Console** — Modify application details (App Name, Logo, Favicon), live mail servers (SMTP settings with built-in Test Email utility), and authentication methods in the browser. Kept inside database configurations with memory caching for fast processing.
* 📁 **Asynchronous Bulk Actions** — Integrated memory-friendly bulk export and import using `maatwebsite/excel` under queuing, along with bulk archiving, restoration, and permanent removal.
* 🌐 **Global App Search Engine** — An intelligent keyboard-navigable global search system (`/api/search`) indexing users, roles, system settings, and notifications instantly.
* 🔌 **Enterprise OAuth2 & SSO Server** — Built-in Laravel Passport endpoints to integrate secure Single Sign-On (SSO) tokens with secondary platforms or mobile applications.
With **Docker** running on your machine, simply execute the following command at the root of the project: ---
## 🛠️ Tech Stack & Dependencies
| Layer | Technology | Version | Description |
|---|---|---|---|
| **Core Framework** | Laravel | `11.x` | Modern backend routing, queues, and container |
| **Frontend Runtime** | React | `18.x` | Declarative UI layer written in TypeScript |
| **Design Engine** | TailwindCSS | `v4.x` | Ultra-fast utility CSS engine |
| **Bridge Engine** | Inertia.js | `v2.x` | Classic routing mechanics with dynamic SPA feel |
| **API Authentication** | Laravel Sanctum | `v4.x` | Fast SPA and mobile API session token auth |
| **OAuth2 / SSO** | Laravel Passport | `v12.x` | Heavy-duty OAuth client authorization servers |
| **Roles & Privileges** | Spatie Permissions | `v6.x` | Granular permission layers using Laravel Gates |
| **Audit Logs** | Spatie Activity Logs | `v4.x` | Detailed logging for DB models and user actions |
| **Docs Generator** | Scribe | `v4.x` | Dynamic API markdown/HTML documentation builder |
---
## 📂 Directory Structure Overview
This project follows clean code conventions and modular MVC architectures:
```text
├── app/
│ ├── Http/
│ │ ├── Controllers/ # Versioned REST Controllers & SPA Action Handlers
│ │ ├── Middleware/ # 2FA checks, CORS, rate limits, and custom gates
│ │ └── Requests/ # Fully-validated Form requests
│ └── Models/ # Database models (User, Setting, RemoteConfig, NotificationLog)
├── bootstrap/
│ └── cache/ # Optimized system boot caching configurations
├── config/ # Consolidated application parameters
├── database/
│ ├── migrations/ # Versioned SQL migrations schema
│ └── seeders/ # Auto-populating test profiles & RBAC setups
├── docker/ # Custom multi-arch Dockerfiles (PHP 8.3 configurations)
├── public/ # Compiled Vite assets, logos, and entry points
├── resources/
│ ├── css/ # Global style variables and animations
│ ├── js/
│ │ ├── Components/ # Reusable UI building blocks (DataTable, Modal, Checkbox)
│ │ ├── Contexts/ # State hooks (ToastContext)
│ │ ├── Layouts/ # Sidebars, Navbars, dynamic layout bindings
│ │ └── Pages/ # Individual React single-page routes
│ └── views/ # Blade server-side templates and layout gates
├── routes/
│ ├── api.php # Token-protected versioned endpoint routing
│ ├── auth.php # Login/registration workflows and security challenges
│ └── web.php # Application administration routes
└── run.sh # Automated unified terminal start dashboard
```
---
## ⚡ Quick Start & Automation
This project is fully containerized and features a unified shell script that automates compilation, migration, containerization, and initialization.
### Prerequisites
Make sure **Docker Desktop** is running on your device.
### Spin Up
Simply execute the following command at the root of the project:
```bash ```bash
./run.sh ./run.sh
``` ```
This script will completely automate the setup by: > [!NOTE]
1. Creating a `.env` file from `.env.example` (if it does not exist yet). > **What the `run.sh` script automates for you:**
2. Starting the PostgreSQL and Redis containers in the background. > 1. Verifies/creates a local `.env` configuration file from `.env.example`.
3. Installing Composer dependencies. > 2. Starts PostgreSQL and Redis containers in the background.
4. Generating the application encryption key. > 3. Installs Composer packages and frontend Node modules (`npm install`).
5. Running all database migrations and seeding the default accounts. > 4. Generates the application key and builds the Passport OAuth client keys.
6. Installing Node.js (NPM) frontend dependencies. > 5. Runs database migrations and seeds the database with roles and default users.
7. Starting the development server (`php artisan serve` + `Vite` + queue listeners + logs) concurrently. > 6. Launches the development servers (`Artisan serve` + `Vite` + queue listeners + logs) concurrently in a single dashboard!
--- ---
### Manual Setup (Without Automation Script) ### 🔧 Manual Setup (Without Automation Script)
If you prefer to perform the setup manually: If you prefer to perform the setup step-by-step:
1. **Spin up database & cache services:** 1. **Spin up database & cache services:**
```bash ```bash
@@ -57,89 +119,144 @@ If you prefer to perform the setup manually:
```bash ```bash
php artisan migrate --seed php artisan migrate --seed
``` ```
5. **Install frontend dependencies & start dev server:** 5. **Install frontend dependencies & build assets:**
```bash ```bash
npm install npm install
composer dev npm run dev
``` ```
---
## Default Credentials ## 🔐 Default Credentials
| Role | Email | Password | Use the default credentials below to test the RBAC capabilities of the starter kit:
|---|---|---|
| super-admin | superadmin@biiskit.com | password |
| admin | admin@biiskit.com | password |
| user | user@biiskit.com | password |
## Roles & Permissions | Role | Email | Password | Role Features |
|---|---|---|---|
| **super-admin** | `superadmin@biiskit.com` | `password` | Complete access. Bypasses all authority gates globally. |
| **admin** | `admin@biiskit.com` | `password` | Management privileges for users, roles, and logs. |
| **user** | `user@biiskit.com` | `password` | Standard user dashboard with read-only dashboard widgets. |
---
## 🛡️ Roles & Permissions Matrix
The default permission matrix seeded during setup is as follows:
| Permission | super-admin | admin | user | | Permission | super-admin | admin | user |
|---|:---:|:---:|:---:| |---|:---:|:---:|:---:|
| user.view | ✓ | ✓ | ✓ | | `user.view` | ✓ | ✓ | ✓ |
| user.create | ✓ | ✓ | — | | `user.create` | ✓ | ✓ | — |
| user.edit | ✓ | ✓ | — | | `user.edit` | ✓ | ✓ | — |
| user.delete | ✓ | ✓ | — | | `user.delete` | ✓ | ✓ | — |
| role.view | ✓ | ✓ | — | | `role.view` | ✓ | ✓ | — |
| role.manage | ✓ | ✓ | — | | `role.manage` | ✓ | ✓ | — |
| settings.manage | ✓ | — | — | | `settings.manage` | ✓ | — | — |
`super-admin` bypasses all checks via `Gate::before`. ---
## Features ## 🌎 Dynamic System Settings (Super-Admin Console)
- **User Management** — CRUD, soft delete, restore, bulk export/import (Excel/CSV), avatar upload Accessible at `/system-settings` for users holding the `super-admin` role, this panel allows you to customize the core parameters in real-time:
- **Role & Permission Management** — Assign roles, fine-grained permission matrix UI
- **Activity Logs** — Auto-logged actions via spatie/activitylog, filterable, clearable
- **Notifications** — Admin broadcast notifications with read/unread tracking
- **Two-Factor Auth** — TOTP 2FA (Google Authenticator compatible), enable/disable per user via Account Settings, recovery codes, full login challenge flow
- **Account Settings** — Profile, avatar, phone, bio, password change, 2FA management, account deletion — with tab state persisted in URL hash
- **System Settings** — App name, branding, mail/SMTP, OAuth (Google/GitHub), password rules, mobile app version gate — stored in DB, cached; super-admin only
- **Remote Config** — Mobile app version gate (`GET /api/v1/app/config?platform=android`)
- **Branded Error Pages** — Inertia-rendered 403, 404, 419, 500, 503
- **API** — Versioned REST API (`/api/v1/*`) with Sanctum token auth + rate limiting
- **OAuth2/SSO** — Laravel Passport endpoints for third-party app integration
- **In-app Documentation** — Full feature docs at `/documentation` (accessible via sidebar)
## Environment Variables * **Custom App Branding** — Change app title, header logos, and tab favicon. The UI adapts dynamically.
* **Live Mail Configuration** — Manage SMTP host, port, username, password, and sender credentials. Features a **Test SMTP Email** utility to immediately verify outbound mailing settings.
* **OAuth Login Toggles** — Instantly enable or disable Google/GitHub Single Sign-On (SSO) gateways.
* **Password Policy Enforcer** — Dynamically adjust password complexity requirements (minimum length, mixed-case, numbers, special characters).
* **Mobile Gatekeeper** — Configure API version parameters and remote variables for client mobile apps.
Key variables beyond the Laravel defaults: ---
```env ## 🔌 API Endpoints Reference (v1)
# Mail (overridable via System Settings UI)
MAIL_MAILER=smtp
MAIL_HOST=
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
# OAuth (Passport) All endpoints listed below are versioned and located under `/api/v1/*`. Requests requesting authorization require a header formatted as `Authorization: Bearer <your_token>`.
PASSPORT_PERSONAL_ACCESS_CLIENT_ID=
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=
```
## API Endpoints (v1)
### Authentication Gateways
| Method | Endpoint | Auth | Description | | Method | Endpoint | Auth | Description |
|---|---|---|---| |---|---|---|---|
| POST | `/api/v1/login` | — | Get Bearer token (rate-limited: 10/min) | | `POST` | `/api/v1/login` | — | Exchange credentials for Bearer Token (Rate limited) |
| POST | `/api/v1/logout` | Bearer | Revoke token | | `POST` | `/api/v1/logout` | Bearer | Revoke current authenticated session token |
| GET | `/api/v1/me` | Bearer | Authenticated user with roles & permissions | | `GET` | `/api/v1/me` | Bearer | Fetch authenticated user data, roles, and permissions |
| GET | `/api/v1/users` | Bearer | List users (paginated, sortable, filterable) |
| POST | `/api/v1/users` | Bearer | Create user |
| GET | `/api/v1/users/{id}` | Bearer | Get user |
| PATCH | `/api/v1/users/{id}` | Bearer | Update user |
| DELETE | `/api/v1/users/{id}` | Bearer | Soft-delete user |
| POST | `/api/v1/users/{id}/restore` | Bearer | Restore user |
| DELETE | `/api/v1/users/{id}/force` | Bearer | Permanent delete |
| GET | `/api/v1/app-config` | — | Mobile remote config |
Full interactive docs: `GET /documentation` ### User Management
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| `GET` | `/api/v1/users` | Bearer | Retrieve paginated users (sortable & filterable) |
| `POST` | `/api/v1/users` | Bearer | Create a new user record |
| `GET` | `/api/v1/users/{id}` | Bearer | Get details of a specific user |
| `PATCH` | `/api/v1/users/{id}` | Bearer | Update user profile details |
| `DELETE` | `/api/v1/users/{id}` | Bearer | Soft-delete a user record |
| `POST` | `/api/v1/users/{id}/restore` | Bearer | Restore a soft-deleted user |
| `DELETE` | `/api/v1/users/{id}/force` | Bearer | Permanently delete a user record |
## Running Tests ### Remote Mobile App Configurations
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| `GET` | `/api/v1/app-config` | — | Retrieve mobile app remote configuration parameters |
---
## 🧪 Comprehensive Automated Testing
Ensure all features remain perfectly healthy by running the comprehensive Pest / PHPUnit suite:
```bash ```bash
php artisan test php artisan test
# or with coverage: ```
Or evaluate coverage scores:
```bash
php artisan test --coverage php artisan test --coverage
``` ```
---
## 📂 Project Directory Structure
```text
.
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Api/V1/ # Sanctum-protected REST API endpoints
│ │ │ ├── Auth/ # Full Breeze web and 2FA authentication flow
│ │ │ ├── Settings/ # Dynamic system configurations and branding controllers
│ │ │ ├── Dashboard/ # Dynamic home widgets layout engine
│ │ │ └── Profile/ # Account preferences and session settings
│ │ └── Middleware/ # Custom CORS, IP blockers, 2FA enforcement
│ ├── Models/ # User, Role, Permission, SystemSetting, ActivityLog, DashboardWidget
│ └── Services/ # Dynamic configuration caches, Excel batching, and remote sync services
├── bootstrap/ # Compiled route/config caches
├── config/ # Core configs (Spatie matrix, Breeze auth, Inertia, Mail)
├── database/
│ ├── migrations/ # Standardized DB schemas (users, roles, permissions, settings, logs)
│ └── seeders/ # Initial dynamic settings & full RBAC Matrix seeds
├── docker/ # Optimized Alpine + PHP-FPM / pgsql / Redis service images
├── public/ # Compiled browser-ready front-end assets
├── resources/
│ ├── js/
│ │ ├── Components/ # Reusable dynamic components (Command Palette, Modals, Forms)
│ │ ├── Layouts/ # Premium dashboard frames, notification panel, global search
│ │ └── Pages/ # Sleek React + TailwindCSS views (RBAC, Audit Logs, Settings)
│ └── views/ # Primary Inertia wrapper template
├── routes/ # Structured API, Web, Auth, and System configuration endpoints
├── storage/ # Dynamic file assets, private exports, and system logs
└── tests/ # Full-featured integration and regression test coverage
```
---
## 🐳 Architecture & Self-Healing Orchestration (`run.sh`)
The starter kit features an advanced orchestration script (`run.sh`) that automates container configuration and implements robust **Self-Healing Mechanics**:
1. **Port & Instance Conflict Protection**: Scans and gracefully stops local/containerized processes conflicting on ports `8000` (Web), `5432` (Postgres), and `6379` (Redis).
2. **Zero-Dependency Host Bootstrapping**: Automatically spins up temporary PHP containers to run `composer install` if dependencies are absent, ensuring you can initialize the stack on a completely clean host.
3. **Database Health Synchronization**: Implements asynchronous health loops checking container states. Database migrations and seeder processes wait precisely until services report a `healthy` state.
4. **Automatic Workspace Permissions**: Secures and corrects directory owner attributes (`chown` / `chmod`) across compiled Vite bundles and Laravel cache paths.
---
## 📄 License & Terms
Proprietary © 2026 Andika Debi Putra (Debesocial). Designed and packaged to expedite development while aligning with modern security and architectural guidelines. All rights reserved.
@@ -10,7 +10,7 @@ class ActivityLogController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$this->authorize('user.view'); abort_if(!auth()->user()->can('activity-logs.view'), 403, 'Unauthorized. Activity logs view permission required.');
$search = $request->input('search'); $search = $request->input('search');
$logName = $request->input('log_name'); $logName = $request->input('log_name');
@@ -58,7 +58,7 @@ class ActivityLogController extends Controller
public function bulkDelete(Request $request) public function bulkDelete(Request $request)
{ {
$this->authorize('user.delete'); abort_if(!auth()->user()->can('activity-logs.delete'), 403, 'Unauthorized. Activity logs delete permission required.');
$ids = (array) $request->input('ids', []); $ids = (array) $request->input('ids', []);
@@ -35,9 +35,49 @@ class AuthenticatedSessionController extends Controller
$user = Auth::user(); $user = Auth::user();
// If user has 2FA enabled, redirect to challenge screen // Check global 2FA toggles
if ($user->two_factor_confirmed_at && $user->two_factor_secret) { $totpAllowed = true;
$emailAllowed = true;
try {
$settings = \Illuminate\Support\Facades\Cache::rememberForever('system_settings', function () {
return \App\Models\Setting::all()->pluck('value', 'key')->toArray();
});
if (isset($settings['two_factor_totp_enabled'])) {
$totpAllowed = $settings['two_factor_totp_enabled'] === '1' || $settings['two_factor_totp_enabled'] === true;
}
if (isset($settings['two_factor_email_enabled'])) {
$emailAllowed = $settings['two_factor_email_enabled'] === '1' || $settings['two_factor_email_enabled'] === true;
}
} catch (\Exception $e) {
// DB not ready or migrated
}
// If user has 2FA enabled, and it's globally allowed, redirect to challenge screen
if ($totpAllowed && $user->two_factor_confirmed_at && $user->two_factor_secret) {
$request->session()->put('two_factor_user_id', $user->id); $request->session()->put('two_factor_user_id', $user->id);
$request->session()->put('two_factor_type', 'totp');
Auth::guard('web')->logout();
$request->session()->forget('password_hash_web');
return redirect()->route('two-factor.challenge');
}
// If user has Email 2FA enabled, and it's globally allowed, redirect to email challenge
if ($emailAllowed && $user->email_2fa_enabled) {
$code = str_pad(mt_rand(100000, 999999), 6, '0', STR_PAD_LEFT);
$user->update([
'email_2fa_code' => $code,
'email_2fa_expires_at' => now()->addMinutes(10),
]);
try {
\Illuminate\Support\Facades\Mail::to($user->email)->send(new \App\Mail\Send2FACode($code));
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error("Failed to send 2FA Email Code: " . $e->getMessage());
}
$request->session()->put('two_factor_user_id', $user->id);
$request->session()->put('two_factor_type', 'email');
Auth::guard('web')->logout(); Auth::guard('web')->logout();
$request->session()->forget('password_hash_web'); $request->session()->forget('password_hash_web');
@@ -12,6 +12,8 @@ class NotificationController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
abort_if(!auth()->user()->can('notifications.view'), 403, 'Unauthorized. Notification view permission required.');
$logs = NotificationLog::with(['targetUser', 'sender']) $logs = NotificationLog::with(['targetUser', 'sender'])
->latest() ->latest()
->paginate(10); ->paginate(10);
@@ -37,6 +39,8 @@ class NotificationController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
abort_if(!auth()->user()->can('notifications.send'), 403, 'Unauthorized. Notification send permission required.');
$validated = $request->validate([ $validated = $request->validate([
'title' => 'required|string|max:255', 'title' => 'required|string|max:255',
'body' => 'required|string', 'body' => 'required|string',
+8
View File
@@ -11,6 +11,8 @@ class RoleController extends Controller
{ {
public function index() public function index()
{ {
abort_if(!auth()->user()->can('role.view'), 403, 'Unauthorized. Role view permission required.');
$order = ['super-admin' => 0, 'admin' => 1, 'user' => 2]; $order = ['super-admin' => 0, 'admin' => 1, 'user' => 2];
$roles = Role::where('guard_name', 'web') $roles = Role::where('guard_name', 'web')
@@ -48,6 +50,8 @@ class RoleController extends Controller
*/ */
public function updatePermissions(Request $request, Role $role) public function updatePermissions(Request $request, Role $role)
{ {
abort_if(!auth()->user()->can('role.manage'), 403, 'Unauthorized. Role management permission required.');
$validated = $request->validate([ $validated = $request->validate([
'permissions' => 'required|array', 'permissions' => 'required|array',
'permissions.*' => 'string|exists:permissions,name', 'permissions.*' => 'string|exists:permissions,name',
@@ -64,6 +68,8 @@ class RoleController extends Controller
*/ */
public function store(Request $request) public function store(Request $request)
{ {
abort_if(!auth()->user()->can('role.create'), 403, 'Unauthorized. Role creation permission required.');
$validated = $request->validate([ $validated = $request->validate([
'name' => 'required|string|max:50|unique:roles,name', 'name' => 'required|string|max:50|unique:roles,name',
]); ]);
@@ -81,6 +87,8 @@ class RoleController extends Controller
*/ */
public function destroy(Role $role) public function destroy(Role $role)
{ {
abort_if(!auth()->user()->can('role.delete'), 403, 'Unauthorized. Role deletion permission required.');
if ($role->name === 'super-admin') { if ($role->name === 'super-admin') {
return back()->withErrors(['error' => 'Cannot delete the super-admin role.']); return back()->withErrors(['error' => 'Cannot delete the super-admin role.']);
} }
@@ -34,13 +34,49 @@ class SettingsController extends Controller
$qrCode = 'data:image/svg+xml;base64,' . base64_encode((new \BaconQrCode\Writer($renderer))->writeString($otpUrl)); $qrCode = 'data:image/svg+xml;base64,' . base64_encode((new \BaconQrCode\Writer($renderer))->writeString($otpUrl));
} }
$mailDriver = config('mail.default');
$mailHost = \App\Models\Setting::where('key', 'mail_host')->first()?->value ?: config('mail.mailers.smtp.host');
$smtpConfigured = false;
if ($mailDriver === 'log') {
$smtpConfigured = true;
} elseif ($mailHost === 'mailpit') {
$smtpConfigured = true;
} else {
$mailUsername = \App\Models\Setting::where('key', 'mail_username')->first()?->value ?: config('mail.mailers.smtp.username');
$mailPassword = \App\Models\Setting::where('key', 'mail_password')->first()?->value ?: config('mail.mailers.smtp.password');
$smtpConfigured = !empty($mailHost) && !empty($mailUsername) && !empty($mailPassword);
}
$totpAllowed = true;
$emailAllowed = true;
try {
$systemSettings = \Illuminate\Support\Facades\Cache::rememberForever('system_settings', function () {
return \App\Models\Setting::all()->pluck('value', 'key')->toArray();
});
if (isset($systemSettings['two_factor_totp_enabled'])) {
$totpAllowed = $systemSettings['two_factor_totp_enabled'] === '1' || $systemSettings['two_factor_totp_enabled'] === true;
}
if (isset($systemSettings['two_factor_email_enabled'])) {
$emailAllowed = $systemSettings['two_factor_email_enabled'] === '1' || $systemSettings['two_factor_email_enabled'] === true;
}
} catch (\Exception $e) {
// DB not ready or migrated
}
return Inertia::render('Settings/Index', [ return Inertia::render('Settings/Index', [
'mustVerifyEmail' => $user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail, 'mustVerifyEmail' => $user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail,
'status' => session('status'), 'status' => session('status'),
'twoFactorSettings' => [
'totp_allowed' => $totpAllowed,
'email_allowed' => $emailAllowed,
],
'twoFactor' => [ 'twoFactor' => [
'enabled' => $twoFactorEnabled, 'enabled' => $twoFactorEnabled,
'qr_code' => $qrCode, 'qr_code' => $qrCode,
'secret' => $secret, 'secret' => $secret,
'email_enabled' => (bool)$user->email_2fa_enabled,
'smtp_configured' => $smtpConfigured,
'recovery_codes' => $user->two_factor_recovery_codes 'recovery_codes' => $user->two_factor_recovery_codes
? json_decode($user->two_factor_recovery_codes, true) ? json_decode($user->two_factor_recovery_codes, true)
: [], : [],
@@ -15,7 +15,7 @@ class SystemSettingController extends Controller
*/ */
public function index() public function index()
{ {
abort_if(!auth()->user()->hasRole('super-admin'), 403, 'Unauthorized. Super-Admin only.'); abort_if(!auth()->user()->hasRole('super-admin') && !auth()->user()->can('settings.view'), 403, 'Unauthorized. Settings view permission required.');
$settings = Setting::all()->pluck('value', 'key'); $settings = Setting::all()->pluck('value', 'key');
@@ -31,6 +31,10 @@ class SystemSettingController extends Controller
'password_require_numbers' => '0', 'password_require_numbers' => '0',
'password_require_mixed_case' => '0', 'password_require_mixed_case' => '0',
// Two Factor global toggles
'two_factor_totp_enabled' => '1',
'two_factor_email_enabled' => '1',
// OAuth // OAuth
'oauth_google_enabled' => '0', 'oauth_google_enabled' => '0',
'oauth_google_client_id' => '', 'oauth_google_client_id' => '',
@@ -69,7 +73,7 @@ class SystemSettingController extends Controller
*/ */
public function update(Request $request) public function update(Request $request)
{ {
abort_if(!auth()->user()->hasRole('super-admin'), 403, 'Unauthorized. Super-Admin only.'); abort_if(!auth()->user()->hasRole('super-admin') && !auth()->user()->can('settings.edit'), 403, 'Unauthorized. Settings edit permission required.');
$validated = $request->validate([ $validated = $request->validate([
'settings' => 'required|array', 'settings' => 'required|array',
@@ -83,6 +87,9 @@ class SystemSettingController extends Controller
'settings.password_require_numbers' => 'boolean', 'settings.password_require_numbers' => 'boolean',
'settings.password_require_mixed_case' => 'boolean', 'settings.password_require_mixed_case' => 'boolean',
'settings.two_factor_totp_enabled' => 'boolean',
'settings.two_factor_email_enabled' => 'boolean',
'settings.oauth_google_enabled' => 'boolean', 'settings.oauth_google_enabled' => 'boolean',
'settings.oauth_google_client_id' => 'nullable|string', 'settings.oauth_google_client_id' => 'nullable|string',
'settings.oauth_google_client_secret' => 'nullable|string', 'settings.oauth_google_client_secret' => 'nullable|string',
@@ -133,7 +140,7 @@ class SystemSettingController extends Controller
*/ */
public function testEmail(Request $request) public function testEmail(Request $request)
{ {
abort_if(!auth()->user()->hasRole('super-admin'), 403, 'Unauthorized. Super-Admin only.'); abort_if(!auth()->user()->hasRole('super-admin') && !auth()->user()->can('settings.test-email'), 403, 'Unauthorized. SMTP testing permission required.');
$request->validate([ $request->validate([
'recipient' => 'required|email', 'recipient' => 'required|email',
+141 -4
View File
@@ -56,11 +56,45 @@ class TwoFactorController extends Controller
]); ]);
} }
private function isTotpAllowed(): bool
{
try {
$settings = \Illuminate\Support\Facades\Cache::rememberForever('system_settings', function () {
return \App\Models\Setting::all()->pluck('value', 'key')->toArray();
});
if (isset($settings['two_factor_totp_enabled'])) {
return $settings['two_factor_totp_enabled'] === '1' || $settings['two_factor_totp_enabled'] === true;
}
} catch (\Exception $e) {
// DB not ready or migrated
}
return true;
}
private function isEmailAllowed(): bool
{
try {
$settings = \Illuminate\Support\Facades\Cache::rememberForever('system_settings', function () {
return \App\Models\Setting::all()->pluck('value', 'key')->toArray();
});
if (isset($settings['two_factor_email_enabled'])) {
return $settings['two_factor_email_enabled'] === '1' || $settings['two_factor_email_enabled'] === true;
}
} catch (\Exception $e) {
// DB not ready or migrated
}
return true;
}
/** /**
* Confirm & enable 2FA. * Confirm & enable 2FA.
*/ */
public function enable(Request $request) public function enable(Request $request)
{ {
if (!$this->isTotpAllowed()) {
abort(403, 'Google Authenticator (TOTP) is globally disabled by the administrator.');
}
$request->validate([ $request->validate([
'code' => 'required|string', 'code' => 'required|string',
]); ]);
@@ -80,6 +114,7 @@ class TwoFactorController extends Controller
$user->update([ $user->update([
'two_factor_confirmed_at' => now(), 'two_factor_confirmed_at' => now(),
'two_factor_recovery_codes' => json_encode($recoveryCodes->toArray()), 'two_factor_recovery_codes' => json_encode($recoveryCodes->toArray()),
'email_2fa_enabled' => false, // Automatically disable Email 2FA
]); ]);
return back()->with('success', 'Two-Factor Authentication has been enabled successfully.'); return back()->with('success', 'Two-Factor Authentication has been enabled successfully.');
@@ -103,6 +138,49 @@ class TwoFactorController extends Controller
return back()->with('success', 'Two-Factor Authentication has been disabled.'); return back()->with('success', 'Two-Factor Authentication has been disabled.');
} }
public function toggleEmail(Request $request)
{
$request->validate([
'password' => 'required|current_password',
'enabled' => 'required|boolean',
]);
if ($request->enabled && !$this->isEmailAllowed()) {
abort(403, 'Email Two-Factor Authentication is globally disabled by the administrator.');
}
$user = auth()->user();
if ($request->enabled) {
// Live-verify SMTP configuration by sending a test validation email
try {
\Illuminate\Support\Facades\Mail::to($user->email)->send(
new \App\Mail\Send2FACode('123456')
);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error("SMTP verification failed: " . $e->getMessage());
return back()->withErrors([
'password' => 'Cannot enable Email 2FA: Your SMTP mail configuration is invalid or not working. We tried to send a validation email but failed. Error: ' . $e->getMessage()
]);
}
$user->update([
'email_2fa_enabled' => true,
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed_at' => null,
]);
} else {
$user->update([
'email_2fa_enabled' => false,
]);
}
$status = $request->enabled ? 'enabled' : 'disabled';
return back()->with('success', "Two-Factor Authentication via Email has been {$status} successfully.");
}
/** /**
* Regenerate recovery codes. * Regenerate recovery codes.
*/ */
@@ -126,7 +204,11 @@ class TwoFactorController extends Controller
return redirect()->route('login'); return redirect()->route('login');
} }
return Inertia::render('TwoFactor/Challenge'); $type = $request->session()->get('two_factor_type', 'totp');
return Inertia::render('TwoFactor/Challenge', [
'type' => $type,
]);
} }
/** /**
@@ -139,6 +221,7 @@ class TwoFactorController extends Controller
]); ]);
$userId = $request->session()->get('two_factor_user_id'); $userId = $request->session()->get('two_factor_user_id');
$type = $request->session()->get('two_factor_type', 'totp');
if (!$userId) { if (!$userId) {
return redirect()->route('login'); return redirect()->route('login');
@@ -146,8 +229,29 @@ class TwoFactorController extends Controller
$user = \App\Models\User::find($userId); $user = \App\Models\User::find($userId);
if (!$user || !$user->two_factor_secret) { if (!$user) {
$request->session()->forget('two_factor_user_id'); $request->session()->forget(['two_factor_user_id', 'two_factor_type']);
return redirect()->route('login');
}
if ($type === 'email') {
if (!$user->email_2fa_enabled || !$user->email_2fa_code) {
$request->session()->forget(['two_factor_user_id', 'two_factor_type']);
return redirect()->route('login');
}
if ($user->email_2fa_code !== $request->code || !$user->email_2fa_expires_at || $user->email_2fa_expires_at->isPast()) {
return back()->withErrors(['code' => 'Invalid or expired authentication code. Please try again.']);
}
// Code is valid! Clear it
$user->update([
'email_2fa_code' => null,
'email_2fa_expires_at' => null,
]);
} else {
if (!$user->two_factor_secret) {
$request->session()->forget(['two_factor_user_id', 'two_factor_type']);
return redirect()->route('login'); return redirect()->route('login');
} }
@@ -166,12 +270,45 @@ class TwoFactorController extends Controller
if (!$valid) { if (!$valid) {
return back()->withErrors(['code' => 'Invalid code. Please try again.']); return back()->withErrors(['code' => 'Invalid code. Please try again.']);
} }
}
$request->session()->forget('two_factor_user_id'); $request->session()->forget(['two_factor_user_id', 'two_factor_type']);
\Illuminate\Support\Facades\Auth::login($user); \Illuminate\Support\Facades\Auth::login($user);
$request->session()->regenerate(); $request->session()->regenerate();
return redirect()->intended(route('dashboard')); return redirect()->intended(route('dashboard'));
} }
/**
* Resend Email 2FA verification code.
*/
public function resendCode(Request $request)
{
if (!$request->session()->has('two_factor_user_id') || $request->session()->get('two_factor_type') !== 'email') {
return redirect()->route('login');
}
$userId = $request->session()->get('two_factor_user_id');
$user = \App\Models\User::find($userId);
if (!$user || !$user->email_2fa_enabled) {
return redirect()->route('login');
}
$code = str_pad(mt_rand(100000, 999999), 6, '0', STR_PAD_LEFT);
$user->update([
'email_2fa_code' => $code,
'email_2fa_expires_at' => now()->addMinutes(10),
]);
try {
\Illuminate\Support\Facades\Mail::to($user->email)->send(new \App\Mail\Send2FACode($code));
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error("Failed to resend 2FA Email Code: " . $e->getMessage());
return back()->withErrors(['code' => 'Failed to send email. Please check SMTP configuration or try again.']);
}
return back()->with('success', 'A new verification code has been sent to your email.');
}
} }
+3 -3
View File
@@ -181,7 +181,7 @@ class UserController extends Controller
public function bulkArchive(Request $request) public function bulkArchive(Request $request)
{ {
$this->authorize('user.delete'); abort_if(!auth()->user()->can('user.delete'), 403, 'Unauthorized. User delete permission required.');
$ids = array_filter( $ids = array_filter(
(array) $request->input('ids', []), (array) $request->input('ids', []),
@@ -195,7 +195,7 @@ class UserController extends Controller
public function bulkRestore(Request $request) public function bulkRestore(Request $request)
{ {
$this->authorize('user.delete'); abort_if(!auth()->user()->can('user.restore'), 403, 'Unauthorized. User restore permission required.');
$ids = (array) $request->input('ids', []); $ids = (array) $request->input('ids', []);
@@ -206,7 +206,7 @@ class UserController extends Controller
public function bulkForceDelete(Request $request) public function bulkForceDelete(Request $request)
{ {
$this->authorize('user.delete'); abort_if(!auth()->user()->can('user.force-delete'), 403, 'Unauthorized. User permanent deletion permission required.');
$ids = array_filter( $ids = array_filter(
(array) $request->input('ids', []), (array) $request->input('ids', []),
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class Send2FACode extends Mailable
{
use Queueable, SerializesModels;
public string $code;
public function __construct(string $code)
{
$this->code = $code;
}
public function envelope(): Envelope
{
return new Envelope(
subject: '[' . config('app.name') . '] Two-Factor Authentication Code',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.two-factor-code',
);
}
}
+4 -2
View File
@@ -16,8 +16,8 @@ use Spatie\Permission\Traits\HasRoles;
use Spatie\Activitylog\Traits\LogsActivity; use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\LogOptions;
#[Fillable(['first_name', 'last_name', 'email', 'phone', 'bio', 'password', 'status', 'avatar_url', 'meta', 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'])] #[Fillable(['first_name', 'last_name', 'email', 'phone', 'bio', 'password', 'status', 'avatar_url', 'meta', 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at', 'email_2fa_enabled', 'email_2fa_code', 'email_2fa_expires_at'])]
#[Hidden(['password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes'])] #[Hidden(['password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes', 'email_2fa_code'])]
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<UserFactory> */ /** @use HasFactory<UserFactory> */
@@ -66,6 +66,8 @@ class User extends Authenticatable
'password' => 'hashed', 'password' => 'hashed',
'meta' => 'array', 'meta' => 'array',
'status' => 'string', 'status' => 'string',
'email_2fa_enabled' => 'boolean',
'email_2fa_expires_at' => 'datetime',
]; ];
} }
+2 -2
View File
@@ -33,11 +33,11 @@ class UserPolicy
public function restore(User $authUser, User $user): bool public function restore(User $authUser, User $user): bool
{ {
return $authUser->hasPermissionTo('user.delete'); return $authUser->hasPermissionTo('user.restore');
} }
public function forceDelete(User $authUser, User $user): bool public function forceDelete(User $authUser, User $user): bool
{ {
return $authUser->hasPermissionTo('user.delete') && $authUser->id !== $user->id; return $authUser->hasPermissionTo('user.force-delete') && $authUser->id !== $user->id;
} }
} }
Regular → Executable
View File
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('email_2fa_enabled')->default(false)->after('two_factor_confirmed_at');
$table->string('email_2fa_code')->nullable()->after('email_2fa_enabled');
$table->timestamp('email_2fa_expires_at')->nullable()->after('email_2fa_code');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'email_2fa_enabled',
'email_2fa_code',
'email_2fa_expires_at',
]);
});
}
};
+39 -5
View File
@@ -14,13 +14,37 @@ class RolesAndPermissionsSeeder extends Seeder
app()[PermissionRegistrar::class]->forgetCachedPermissions(); app()[PermissionRegistrar::class]->forgetCachedPermissions();
$permissions = [ $permissions = [
// User Management
'user.view', 'user.view',
'user.create', 'user.create',
'user.edit', 'user.edit',
'user.delete', 'user.delete',
'user.restore',
'user.force-delete',
'user.export',
'user.import',
// Role Management
'role.view', 'role.view',
'role.create',
'role.delete',
'role.manage', 'role.manage',
'settings.manage',
// Notification broadcast
'notifications.view',
'notifications.send',
// Activity Logs
'activity-logs.view',
'activity-logs.delete',
// System Settings
'settings.view',
'settings.edit',
'settings.test-email',
// Internal Documentation
'documentation.view',
]; ];
foreach ($permissions as $permission) { foreach ($permissions as $permission) {
@@ -28,22 +52,32 @@ class RolesAndPermissionsSeeder extends Seeder
Permission::firstOrCreate(['name' => $permission, 'guard_name' => 'api']); Permission::firstOrCreate(['name' => $permission, 'guard_name' => 'api']);
} }
// user — read-only access // user — read-only basic access
$user = Role::firstOrCreate(['name' => 'user', 'guard_name' => 'web']); $user = Role::firstOrCreate(['name' => 'user', 'guard_name' => 'web']);
$user->syncPermissions(['user.view']); $user->syncPermissions([
'user.view',
]);
// admin — full user & role management, no system settings // admin — full operational, governance, and reporting access, no raw system configuration
$admin = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']); $admin = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
$admin->syncPermissions([ $admin->syncPermissions([
'user.view', 'user.view',
'user.create', 'user.create',
'user.edit', 'user.edit',
'user.delete', 'user.delete',
'user.restore',
'user.export',
'user.import',
'role.view', 'role.view',
'role.create',
'role.delete',
'role.manage', 'role.manage',
'notifications.view',
'notifications.send',
'activity-logs.view',
]); ]);
// super-admin — everything (Gate::before bypasses checks anyway) // super-admin — absolute root access
$superAdmin = Role::firstOrCreate(['name' => 'super-admin', 'guard_name' => 'web']); $superAdmin = Role::firstOrCreate(['name' => 'super-admin', 'guard_name' => 'web']);
$superAdmin->syncPermissions(Permission::where('guard_name', 'web')->get()); $superAdmin->syncPermissions(Permission::where('guard_name', 'web')->get());
} }
Executable
+281
View File
@@ -0,0 +1,281 @@
#!/bin/bash
# --- Color Definitions for Premium Terminal Styling ---
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Helper Functions for Clean Output
log_info() {
echo -e " ${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e " ${GREEN}[SUCCESS]${NC}$1"
}
log_warning() {
echo -e " ${YELLOW}[WARNING]${NC}$1"
}
log_error() {
echo -e " ${RED}[ERROR]${NC}$1"
}
clear_screen() {
clear 2>/dev/null || printf "\033c"
}
clear_screen
# Premium Welcome Header
echo -e "${CYAN}======================================================================${NC}"
echo -e "${BOLD}${MAGENTA} 🛠️ BIIPROJECT KIT V2 - CORE APPLICATION INSTALLER 🛠️ ${NC}"
echo -e "${CYAN}======================================================================${NC}"
echo -e " Selamat datang di asisten instalasi otomatis Biiproject Kit."
echo -e " Skrip ini akan memvalidasi lingkungan Anda dan menginstal seluruh"
echo -e " kebutuhan aplikasi agar siap dijalankan."
echo -e "${CYAN}======================================================================${NC}"
echo ""
# 1. PRE-FLIGHT SYSTEM CHECKS
echo -e "${BOLD}${BLUE}[1/7] Memeriksa Kebutuhan Sistem (Pre-flight Checks)...${NC}"
# Check Docker Installation
if ! command -v docker &> /dev/null; then
log_error "Docker tidak ditemukan pada sistem ini."
log_warning "Harap instal Docker terlebih dahulu sebelum melanjutkan."
exit 1
else
log_success "Docker terinstal."
fi
# Check Docker Daemon Status
if ! docker info &> /dev/null; then
log_error "Docker daemon tidak berjalan."
log_warning "Harap jalankan Docker service Anda (misal: sudo systemctl start docker)."
exit 1
else
log_success "Docker daemon aktif dan berjalan."
fi
# Check Docker Compose Installation
if ! docker compose version &> /dev/null; then
log_warning "Docker Compose v2 tidak terdeteksi. Mencoba memeriksa 'docker-compose' lama..."
if ! command -v docker-compose &> /dev/null; then
log_error "Docker Compose tidak ditemukan pada sistem."
exit 1
else
log_success "Docker-compose (v1) terinstal."
DOCKER_COMPOSE_CMD="docker-compose"
fi
else
log_success "Docker Compose v2 terinstal."
DOCKER_COMPOSE_CMD="docker compose"
fi
echo ""
# 2. ENVIRONMENT CONFIGURATION
echo -e "${BOLD}${BLUE}[2/7] Mengonfigurasi File Environment (.env)...${NC}"
if [ ! -f .env ]; then
if [ -f .env.example ]; then
log_info "Membuat file .env baru dari .env.example..."
cp .env.example .env
log_success "File .env berhasil dibuat."
else
log_error "File .env.example tidak ditemukan! Tidak dapat membuat file .env."
exit 1
fi
else
log_info "File .env sudah ada. Menggunakan konfigurasi yang ada."
fi
echo ""
# Parse App Port from .env
APP_PORT=$(grep "^APP_PORT=" .env | cut -d'=' -f2- | tr -d '"'\''')
APP_PORT=${APP_PORT:-8000}
# 3. VENDOR & COMPOSER DEPENDENCIES
echo -e "${BOLD}${BLUE}[3/7] Menginstal Dependensi PHP (Composer)...${NC}"
if [ ! -d vendor ]; then
log_info "Direktori 'vendor' tidak ditemukan. Mengunduh dependensi via PHP-Composer container..."
docker run --rm \
-u "$(id -u):$(id -g)" \
-v "$(pwd):/var/www/html" \
-w /var/www/html \
laravelsail/php83-composer:latest \
composer install --ignore-platform-reqs --no-interaction --prefer-dist
if [ $? -ne 0 ]; then
log_error "Gagal menginstal dependensi Composer. Silakan cek koneksi internet Anda."
exit 1
fi
log_success "Dependensi Composer berhasil diinstal."
else
log_info "Direktori 'vendor' sudah ada. Melewati instalasi Composer."
fi
echo ""
# 4. SPIN UP DOCKER CONTAINERS
echo -e "${BOLD}${BLUE}[4/7] Memulai Container Docker (Database, Redis, Web)...${NC}"
log_info "Menjalankan '$DOCKER_COMPOSE_CMD up -d'..."
$DOCKER_COMPOSE_CMD up -d
if [ $? -ne 0 ]; then
log_error "Gagal menjalankan Docker container. Periksa file docker-compose.yml."
exit 1
fi
log_success "Docker containers berhasil dijalankan."
echo ""
# Resolve container names dynamically
DB_CONTAINER="bii-pgsql"
REDIS_CONTAINER="bii-redis"
WEB_CONTAINER="laravel.test"
# 5. WAITING FOR SERVICES HEALTHY (Postgres & Redis)
echo -e "${BOLD}${BLUE}[5/7] Menunggu Database & Redis Siap (Healthcheck)...${NC}"
echo -ne " ${YELLOW}Memeriksa PostgreSQL Database ($DB_CONTAINER)...${NC}"
RETRIES=0
MAX_RETRIES=40
while [ $RETRIES -lt $MAX_RETRIES ]; do
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$DB_CONTAINER" 2>/dev/null)
if [ "$STATUS" = "healthy" ]; then
echo -e "\r ${GREEN}[SELESAI]${NC} ✔ PostgreSQL siap menerima koneksi! "
break
elif [ "$STATUS" = "unhealthy" ]; then
echo -e "\r ${YELLOW}[PERINGATAN]${NC} ⚠ Healthcheck PGSQL melaporkan error. Melanjutkan..."
break
elif [ -z "$STATUS" ]; then
RUNNING=$(docker inspect --format='{{.State.Running}}' "$DB_CONTAINER" 2>/dev/null)
if [ "$RUNNING" = "true" ]; then
echo -e "\r ${GREEN}[SELESAI]${NC} ✔ PostgreSQL running (tanpa healthcheck status)."
break
fi
fi
echo -n "."
sleep 1.5
RETRIES=$((RETRIES + 1))
done
echo -ne " ${YELLOW}Memeriksa Redis Cache ($REDIS_CONTAINER)...${NC}"
RETRIES=0
while [ $RETRIES -lt $MAX_RETRIES ]; do
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$REDIS_CONTAINER" 2>/dev/null)
if [ "$STATUS" = "healthy" ]; then
echo -e "\r ${GREEN}[SELESAI]${NC} ✔ Redis siap menerima koneksi! "
break
elif [ "$STATUS" = "unhealthy" ]; then
echo -e "\r ${YELLOW}[PERINGATAN]${NC} ⚠ Healthcheck Redis melaporkan error. Melanjutkan..."
break
elif [ -z "$STATUS" ]; then
RUNNING=$(docker inspect --format='{{.State.Running}}' "$REDIS_CONTAINER" 2>/dev/null)
if [ "$RUNNING" = "true" ]; then
echo -e "\r ${GREEN}[SELESAI]${NC} ✔ Redis running (tanpa healthcheck status)."
break
fi
fi
echo -n "."
sleep 1.5
RETRIES=$((RETRIES + 1))
done
sleep 2
echo ""
# 6. INITIALIZING APPLICATION (Key, Database Migrate, NPM)
echo -e "${BOLD}${BLUE}[6/7] Menginisialisasi Database & Aset Aplikasi...${NC}"
# Secure Permissions First
log_info "Mengonfigurasi hak akses file di dalam container..."
$DOCKER_COMPOSE_CMD exec -u root laravel.test chown -R sail:sail /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null
$DOCKER_COMPOSE_CMD exec -u root laravel.test chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null
# Generate Key
APP_KEY_VAL=$(grep "^APP_KEY=" .env | cut -d'=' -f2- | tr -d '"'\''')
if [ -z "$APP_KEY_VAL" ] || [ "$APP_KEY_VAL" = "base64:" ] || [ "$APP_KEY_VAL" = "SomeRandomString" ]; then
log_info "Menghasilkan unique application key..."
$DOCKER_COMPOSE_CMD exec -u sail laravel.test php artisan key:generate
else
log_info "Application key sudah diatur."
fi
# Migration & Seed
log_info "Menjalankan migrasi database dan default seeders..."
$DOCKER_COMPOSE_CMD exec -u sail laravel.test php artisan migrate:fresh --seed
if [ $? -eq 0 ]; then
log_success "Database berhasil di-migrate dan di-seed."
else
log_error "Gagal melakukan migrasi database."
fi
# Link Storage
log_info "Membuat symbolic link untuk storage..."
$DOCKER_COMPOSE_CMD exec -u sail laravel.test php artisan storage:link --quiet 2>/dev/null
log_success "Storage link berhasil terhubung."
# Install NPM Packages
log_info "Memasang dependensi NodeJS (NPM)..."
if [ ! -d node_modules ]; then
$DOCKER_COMPOSE_CMD exec -u sail laravel.test npm install
if [ $? -eq 0 ]; then
log_success "Dependensi NodeJS berhasil dipasang."
else
log_warning "Gagal memasang dependensi NodeJS."
fi
else
log_info "Folder 'node_modules' sudah ada. Melewati instalasi NPM."
fi
# Build Production Assets
log_info "Mengompilasi aset frontend (production build)..."
$DOCKER_COMPOSE_CMD exec -u sail laravel.test npm run build
if [ $? -eq 0 ]; then
log_success "Frontend assets berhasil dikompilasi."
else
log_warning "Gagal melakukan build frontend assets."
fi
# Clean up permissions again in case npm install created any root-owned folders
$DOCKER_COMPOSE_CMD exec -u root laravel.test chown -R sail:sail /var/www/html/node_modules /var/www/html/package-lock.json 2>/dev/null
echo ""
# 7. PERMISSIONS & FINISHING
echo -e "${BOLD}${BLUE}[7/7] Finishing & Finalizing...${NC}"
chmod +x artisan sail run.sh 2>/dev/null
log_success "Hak akses berkas lokal (artisan, sail, run.sh) disesuaikan."
clear_screen
# SUCCESS BOARD
echo -e "${GREEN}======================================================================${NC}"
echo -e "${BOLD}${GREEN} 🎉 INSTALASI BIIPROJECT KIT BERHASIL DISELESAIKAN! 🎉 ${NC}"
echo -e "${GREEN}======================================================================${NC}"
echo -e " Aplikasi Anda sekarang siap digunakan!"
echo -e ""
echo -e " 👉 ${BOLD}URL Aplikasi:${NC} ${CYAN}http://localhost:${APP_PORT}${NC}"
echo -e " 👉 ${BOLD}Monitoring Panel:${NC} ${CYAN}http://localhost:${APP_PORT}/system-monitoring${NC}"
echo -e " 👉 ${BOLD}Dokumentasi API:${NC} ${CYAN}http://localhost:${APP_PORT}/documentation${NC}"
echo -e " 👉 ${BOLD}Layanan Database:${NC} PostgreSQL pada port ${YELLOW}5432${NC}"
echo -e " 👉 ${BOLD}Layanan Redis:${NC} Redis pada port ${YELLOW}6379${NC}"
echo -e ""
echo -e " ========================================================"
echo -e " ${BOLD}CARA MENJALANKAN & MENGHENTIKAN APLIKASI:${NC}"
echo -e " ========================================================"
echo -e " Untuk menjalankan server pengembangan (live reload):"
echo -e " ${CYAN}./run.sh${NC}"
echo -e ""
echo -e " Untuk melihat logs docker secara realtime:"
echo -e " ${CYAN}docker compose logs -f${NC}"
echo -e ""
echo -e " Untuk menghentikan docker container:"
echo -e " ${CYAN}docker compose down${NC}"
echo -e "${GREEN}======================================================================${NC}"
echo ""
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{c as e,o as t,t as n}from"./app-BJ7g6sa8.js";e();var r=n();function i({ability:e,children:n,fallback:i=null}){let{permissions:a}=t().props.auth;return(Array.isArray(e)?e:[e]).some(e=>a.includes(e))?(0,r.jsx)(r.Fragment,{children:n}):(0,r.jsx)(r.Fragment,{children:i})}export{i as t}; import{c as e,o as t,t as n}from"./app-CR33GSSe.js";e();var r=n();function i({ability:e,children:n,fallback:i=null}){let{permissions:a}=t().props.auth;return(Array.isArray(e)?e:[e]).some(e=>a.includes(e))?(0,r.jsx)(r.Fragment,{children:n}):(0,r.jsx)(r.Fragment,{children:i})}export{i as t};
@@ -1,2 +0,0 @@
import{a as e,c as t,d as n,n as r,t as i}from"./app-BJ7g6sa8.js";var a=n(t(),1),o=i();function s(){let[t,n]=(0,a.useState)(!1),{data:i,setData:s,post:c,processing:l,errors:u}=e({code:``});return(0,o.jsxs)(`div`,{className:`min-h-screen bg-[#E3EBE8] flex items-center justify-center p-4`,children:[(0,o.jsx)(r,{title:`Two-Factor Authentication`}),(0,o.jsxs)(`div`,{className:`w-full max-w-sm`,children:[(0,o.jsxs)(`div`,{className:`text-center mb-8`,children:[(0,o.jsx)(`div`,{className:`inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-[#3D4E4B] mb-4`,children:(0,o.jsx)(`svg`,{className:`w-7 h-7 text-[#D4A017]`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2.5,children:(0,o.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`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`})})}),(0,o.jsx)(`h1`,{className:`text-xl font-black text-[#3D4E4B] tracking-tight`,children:`Two-Factor Authentication`}),(0,o.jsx)(`p`,{className:`text-sm text-gray-500 font-medium mt-1`,children:t?`Enter a recovery code to continue`:`Enter the 6-digit code from your authenticator app`})]}),(0,o.jsxs)(`div`,{className:`bg-white rounded-2xl shadow-sm border border-gray-100 p-8`,children:[(0,o.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),c(route(`two-factor.verify`),{preserveScroll:!0})},className:`space-y-5`,children:[(0,o.jsxs)(`div`,{children:[(0,o.jsx)(`label`,{className:`block text-xs font-bold text-gray-500 uppercase tracking-widest mb-2`,children:t?`Recovery Code`:`Authentication Code`}),(0,o.jsx)(`input`,{type:`text`,inputMode:t?`text`:`numeric`,maxLength:t?21:6,value:i.code,onChange:e=>s(`code`,e.target.value),autoFocus:!0,className:`w-full h-12 border rounded-xl px-4 text-center font-mono font-bold text-lg tracking-[0.4em] outline-none transition-all
${u.code?`border-red-300 bg-red-50`:`border-gray-200 focus:border-[#3D4E4B] focus:ring-2 focus:ring-[#3D4E4B]/10`}`,placeholder:t?`xxxxxxxxxx-xxxxxxxxxx`:`000000`}),u.code&&(0,o.jsx)(`p`,{className:`text-xs text-red-500 font-semibold mt-1.5`,children:u.code})]}),(0,o.jsx)(`button`,{type:`submit`,disabled:l||i.code.length<(t?5:6),className:`w-full h-11 bg-[#3D4E4B] text-white text-sm font-bold rounded-xl hover:bg-[#2D3A38] transition-all disabled:opacity-60`,children:l?`Verifying...`:`Continue`})]}),(0,o.jsx)(`div`,{className:`mt-6 text-center`,children:(0,o.jsx)(`button`,{onClick:()=>{n(!t),s(`code`,``)},className:`text-xs font-bold text-[#3D4E4B] hover:underline`,children:t?`Use authenticator code instead`:`Use a recovery code`})})]}),(0,o.jsx)(`div`,{className:`mt-6 text-center`,children:(0,o.jsx)(`a`,{href:`/login`,className:`text-xs font-semibold text-gray-400 hover:text-[#3D4E4B]`,children:`← Back to login`})})]})]})}export{s as default};
@@ -0,0 +1,2 @@
import{a as e,c as t,d as n,i as r,n as i,t as a}from"./app-CR33GSSe.js";import{t as o}from"./swal-DIkHduCM.js";var s=n(t(),1),c=a();function l({type:t=`totp`}){let[n,a]=(0,s.useState)(!1),{data:l,setData:u,post:d,processing:f,errors:p}=e({code:``}),[m,h]=(0,s.useState)(!1),g=e=>{e.preventDefault(),d(route(`two-factor.verify`),{preserveScroll:!0})},_=()=>{m||(h(!0),r.post(route(`two-factor.resend`),{},{preserveScroll:!0,onSuccess:()=>{h(!1),o.success(`Sent!`,`A new verification code has been sent to your email.`)},onError:e=>{h(!1),o.error(`Error`,e.code||`Failed to resend verification code.`)}}))},v=t===`email`;return(0,c.jsxs)(`div`,{className:`min-h-screen bg-[#E3EBE8] flex items-center justify-center p-4`,children:[(0,c.jsx)(i,{title:`Two-Factor Authentication`}),(0,c.jsxs)(`div`,{className:`w-full max-w-sm`,children:[(0,c.jsxs)(`div`,{className:`text-center mb-8`,children:[(0,c.jsx)(`div`,{className:`inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-[#3D4E4B] mb-4 shadow-lg shadow-[#3D4E4B]/10`,children:v&&!n?(0,c.jsx)(`svg`,{className:`w-7 h-7 text-[#D4A017]`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2.5,children:(0,c.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z`})}):(0,c.jsx)(`svg`,{className:`w-7 h-7 text-[#D4A017]`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2.5,children:(0,c.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`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`})})}),(0,c.jsx)(`h1`,{className:`text-xl font-black text-[#3D4E4B] tracking-tight`,children:`Two-Factor Authentication`}),(0,c.jsx)(`p`,{className:`text-sm text-gray-500 font-medium mt-2 leading-relaxed px-2`,children:n?`Enter a recovery code to continue.`:v?`Please enter the 6-digit verification code sent to your registered email address.`:`Please enter the 6-digit authentication code from your authenticator app.`})]}),(0,c.jsxs)(`div`,{className:`bg-white rounded-2xl shadow-sm border border-gray-100 p-8`,children:[(0,c.jsxs)(`form`,{onSubmit:g,className:`space-y-5`,children:[(0,c.jsxs)(`div`,{children:[(0,c.jsx)(`label`,{className:`block text-xs font-bold text-gray-400 uppercase tracking-widest mb-2.5`,children:n?`Recovery Code`:v?`Email Verification Code`:`Authenticator Code`}),(0,c.jsx)(`input`,{type:`text`,inputMode:n?`text`:`numeric`,maxLength:n?21:6,value:l.code,onChange:e=>u(`code`,e.target.value),autoFocus:!0,className:`w-full h-12 border rounded-xl px-4 text-center font-mono font-bold text-lg tracking-[0.4em] outline-none transition-all
${p.code?`border-red-300 bg-red-50 focus:ring-red-100`:`border-gray-200 focus:border-[#3D4E4B] focus:ring-2 focus:ring-[#3D4E4B]/10`}`,placeholder:n?`xxxxxxxxxx-xxxxxxxxxx`:`000000`}),p.code&&(0,c.jsx)(`p`,{className:`text-xs text-red-500 font-semibold mt-1.5 text-center`,children:p.code})]}),(0,c.jsx)(`button`,{type:`submit`,disabled:f||l.code.length<(n?5:6),className:`w-full h-11 bg-[#3D4E4B] text-white text-sm font-bold rounded-xl hover:bg-[#2D3A38] transition-all disabled:opacity-60 shadow-md shadow-[#3D4E4B]/15`,children:f?`Verifying...`:`Continue`})]}),v&&!n&&(0,c.jsx)(`div`,{className:`mt-5 text-center`,children:(0,c.jsx)(`button`,{onClick:_,disabled:m,className:`text-xs font-bold text-[#D4A017] hover:underline disabled:opacity-50`,children:m?`Resending code...`:`Didn't receive a code? Resend`})}),!v&&(0,c.jsx)(`div`,{className:`mt-6 text-center border-t border-gray-50 pt-4`,children:(0,c.jsx)(`button`,{onClick:()=>{a(!n),u(`code`,``)},className:`text-xs font-bold text-[#3D4E4B] hover:underline`,children:n?`Use authenticator code instead`:`Use a recovery code`})})]}),(0,c.jsx)(`div`,{className:`mt-6 text-center`,children:(0,c.jsx)(`a`,{href:`/login`,className:`text-xs font-semibold text-gray-400 hover:text-[#3D4E4B] transition-colors`,children:`← Back to login`})})]})]})}export{l as default};
@@ -1 +1 @@
import{a as e,c as t,n,t as r}from"./app-BJ7g6sa8.js";import{t as i}from"./GuestLayout-CN-YY0cs.js";t();var a=r();function o(){let{data:t,setData:r,post:o,processing:s,errors:c,reset:l}=e({password:``});return(0,a.jsxs)(i,{children:[(0,a.jsx)(n,{title:`Confirm password`}),(0,a.jsxs)(`div`,{className:`mb-8 anim-down`,children:[(0,a.jsx)(`div`,{className:`w-12 h-12 bg-[#3D4E4B]/5 rounded-2xl flex items-center justify-center mb-6`,children:(0,a.jsx)(`svg`,{className:`w-5 h-5 text-[#3D4E4B]`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2,children:(0,a.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,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`})})}),(0,a.jsx)(`h1`,{className:`text-2xl font-bold text-[#1A2421] tracking-tight`,children:`Confirm your password`}),(0,a.jsx)(`p`,{className:`mt-1.5 text-sm text-gray-400 font-medium leading-relaxed`,children:`For your security, please confirm your password to continue.`})]}),(0,a.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),o(route(`password.confirm`),{onFinish:()=>l(`password`)})},className:`anim-up`,style:{animationDelay:`0.1s`},children:[(0,a.jsxs)(`div`,{children:[(0,a.jsx)(`label`,{htmlFor:`password`,className:`block text-sm font-semibold text-gray-600 mb-1.5`,children:`Password`}),(0,a.jsx)(`input`,{id:`password`,type:`password`,autoComplete:`current-password`,autoFocus:!0,value:t.password,onChange:e=>r(`password`,e.target.value),placeholder:`••••••••`,className:`auth-input${c.password?` !border-red-300 !bg-red-50/50`:``}`}),c.password&&(0,a.jsx)(`p`,{className:`mt-1.5 text-xs font-semibold text-red-500`,children:c.password})]}),(0,a.jsx)(`button`,{type:`submit`,disabled:s,className:`mt-6 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed`,children:s?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)(`svg`,{className:`w-4 h-4 animate-spin text-white/60`,fill:`none`,viewBox:`0 0 24 24`,children:[(0,a.jsx)(`circle`,{className:`opacity-25`,cx:`12`,cy:`12`,r:`10`,stroke:`currentColor`,strokeWidth:`4`}),(0,a.jsx)(`path`,{className:`opacity-75`,fill:`currentColor`,d:`M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z`})]}),`Confirming…`]}):`Confirm password`})]})]})}export{o as default}; import{a as e,c as t,n,t as r}from"./app-CR33GSSe.js";import{t as i}from"./GuestLayout-RqG17kT8.js";t();var a=r();function o(){let{data:t,setData:r,post:o,processing:s,errors:c,reset:l}=e({password:``});return(0,a.jsxs)(i,{children:[(0,a.jsx)(n,{title:`Confirm password`}),(0,a.jsxs)(`div`,{className:`mb-8 anim-down`,children:[(0,a.jsx)(`div`,{className:`w-12 h-12 bg-[#3D4E4B]/5 rounded-2xl flex items-center justify-center mb-6`,children:(0,a.jsx)(`svg`,{className:`w-5 h-5 text-[#3D4E4B]`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2,children:(0,a.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,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`})})}),(0,a.jsx)(`h1`,{className:`text-2xl font-bold text-[#1A2421] tracking-tight`,children:`Confirm your password`}),(0,a.jsx)(`p`,{className:`mt-1.5 text-sm text-gray-400 font-medium leading-relaxed`,children:`For your security, please confirm your password to continue.`})]}),(0,a.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),o(route(`password.confirm`),{onFinish:()=>l(`password`)})},className:`anim-up`,style:{animationDelay:`0.1s`},children:[(0,a.jsxs)(`div`,{children:[(0,a.jsx)(`label`,{htmlFor:`password`,className:`block text-sm font-semibold text-gray-600 mb-1.5`,children:`Password`}),(0,a.jsx)(`input`,{id:`password`,type:`password`,autoComplete:`current-password`,autoFocus:!0,value:t.password,onChange:e=>r(`password`,e.target.value),placeholder:`••••••••`,className:`auth-input${c.password?` !border-red-300 !bg-red-50/50`:``}`}),c.password&&(0,a.jsx)(`p`,{className:`mt-1.5 text-xs font-semibold text-red-500`,children:c.password})]}),(0,a.jsx)(`button`,{type:`submit`,disabled:s,className:`mt-6 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed`,children:s?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)(`svg`,{className:`w-4 h-4 animate-spin text-white/60`,fill:`none`,viewBox:`0 0 24 24`,children:[(0,a.jsx)(`circle`,{className:`opacity-25`,cx:`12`,cy:`12`,r:`10`,stroke:`currentColor`,strokeWidth:`4`}),(0,a.jsx)(`path`,{className:`opacity-75`,fill:`currentColor`,d:`M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z`})]}),`Confirming…`]}):`Confirm password`})]})]})}export{o as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{c as e,n as t,r as n,t as r}from"./app-BJ7g6sa8.js";e();var i=r(),a={403:{title:`Access Denied`,description:`You don't have permission to access this resource.`},404:{title:`Page Not Found`,description:`The page you're looking for doesn't exist or has been moved.`},419:{title:`Session Expired`,description:`Your session has expired. Please refresh and try again.`},500:{title:`Server Error`,description:`Something went wrong on our end. Please try again later.`},503:{title:`Under Maintenance`,description:`The system is temporarily unavailable. Please check back soon.`}};function o({status:e}){let{title:r,description:o}=a[e]??{title:`Unexpected Error`,description:`An unexpected error occurred.`};return(0,i.jsxs)(`div`,{className:`min-h-screen bg-[#E3EBE8] flex items-center justify-center p-6`,children:[(0,i.jsx)(t,{title:`${e}${r}`}),(0,i.jsxs)(`div`,{className:`w-full max-w-md`,children:[(0,i.jsxs)(`div`,{className:`bg-white rounded-3xl border border-gray-100 shadow-sm overflow-hidden`,children:[(0,i.jsxs)(`div`,{className:`bg-[#3D4E4B] px-8 py-10 text-center`,children:[(0,i.jsx)(`div`,{className:`text-7xl font-black text-white/10 tracking-tighter leading-none select-none`,children:e}),(0,i.jsx)(`div`,{className:`mt-2 text-xl font-bold text-white tracking-tight`,children:r})]}),(0,i.jsxs)(`div`,{className:`px-8 py-8 text-center space-y-6`,children:[(0,i.jsx)(`p`,{className:`text-sm font-semibold text-gray-400 leading-relaxed`,children:o}),(0,i.jsxs)(`div`,{className:`flex flex-col sm:flex-row gap-3 justify-center`,children:[e===419?(0,i.jsxs)(`button`,{onClick:()=>window.location.reload(),className:`h-10 px-6 bg-[#3D4E4B] text-white text-sm font-bold tracking-tight rounded-xl hover:bg-[#2D3A38] transition-all flex items-center justify-center gap-2`,children:[(0,i.jsx)(`svg`,{className:`w-4 h-4`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2.5,children:(0,i.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15`})}),`Refresh Page`]}):(0,i.jsxs)(n,{href:`/dashboard`,className:`h-10 px-6 bg-[#3D4E4B] text-white text-sm font-bold tracking-tight rounded-xl hover:bg-[#2D3A38] transition-all flex items-center justify-center gap-2`,children:[(0,i.jsx)(`svg`,{className:`w-4 h-4`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2.5,children:(0,i.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6`})}),`Back to Dashboard`]}),(0,i.jsx)(`button`,{onClick:()=>window.history.back(),className:`h-10 px-6 bg-white border border-gray-100 text-gray-500 text-sm font-bold tracking-tight rounded-xl hover:bg-gray-50 transition-all`,children:`Go Back`})]})]})]}),(0,i.jsxs)(`p`,{className:`text-center text-xs font-bold text-gray-400 uppercase tracking-widest mt-6`,children:[`Error `,e]})]})]})}export{o as default}; import{c as e,n as t,r as n,t as r}from"./app-CR33GSSe.js";e();var i=r(),a={403:{title:`Access Denied`,description:`You don't have permission to access this resource.`},404:{title:`Page Not Found`,description:`The page you're looking for doesn't exist or has been moved.`},419:{title:`Session Expired`,description:`Your session has expired. Please refresh and try again.`},500:{title:`Server Error`,description:`Something went wrong on our end. Please try again later.`},503:{title:`Under Maintenance`,description:`The system is temporarily unavailable. Please check back soon.`}};function o({status:e}){let{title:r,description:o}=a[e]??{title:`Unexpected Error`,description:`An unexpected error occurred.`};return(0,i.jsxs)(`div`,{className:`min-h-screen bg-[#E3EBE8] flex items-center justify-center p-6`,children:[(0,i.jsx)(t,{title:`${e}${r}`}),(0,i.jsxs)(`div`,{className:`w-full max-w-md`,children:[(0,i.jsxs)(`div`,{className:`bg-white rounded-3xl border border-gray-100 shadow-sm overflow-hidden`,children:[(0,i.jsxs)(`div`,{className:`bg-[#3D4E4B] px-8 py-10 text-center`,children:[(0,i.jsx)(`div`,{className:`text-7xl font-black text-white/10 tracking-tighter leading-none select-none`,children:e}),(0,i.jsx)(`div`,{className:`mt-2 text-xl font-bold text-white tracking-tight`,children:r})]}),(0,i.jsxs)(`div`,{className:`px-8 py-8 text-center space-y-6`,children:[(0,i.jsx)(`p`,{className:`text-sm font-semibold text-gray-400 leading-relaxed`,children:o}),(0,i.jsxs)(`div`,{className:`flex flex-col sm:flex-row gap-3 justify-center`,children:[e===419?(0,i.jsxs)(`button`,{onClick:()=>window.location.reload(),className:`h-10 px-6 bg-[#3D4E4B] text-white text-sm font-bold tracking-tight rounded-xl hover:bg-[#2D3A38] transition-all flex items-center justify-center gap-2`,children:[(0,i.jsx)(`svg`,{className:`w-4 h-4`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2.5,children:(0,i.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15`})}),`Refresh Page`]}):(0,i.jsxs)(n,{href:`/dashboard`,className:`h-10 px-6 bg-[#3D4E4B] text-white text-sm font-bold tracking-tight rounded-xl hover:bg-[#2D3A38] transition-all flex items-center justify-center gap-2`,children:[(0,i.jsx)(`svg`,{className:`w-4 h-4`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2.5,children:(0,i.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6`})}),`Back to Dashboard`]}),(0,i.jsx)(`button`,{onClick:()=>window.history.back(),className:`h-10 px-6 bg-white border border-gray-100 text-gray-500 text-sm font-bold tracking-tight rounded-xl hover:bg-gray-50 transition-all`,children:`Go Back`})]})]})]}),(0,i.jsxs)(`p`,{className:`text-center text-xs font-bold text-gray-400 uppercase tracking-widest mt-6`,children:[`Error `,e]})]})]})}export{o as default};
@@ -1 +1 @@
import{a as e,c as t,n,r,t as i}from"./app-BJ7g6sa8.js";import{t as a}from"./GuestLayout-CN-YY0cs.js";t();var o=i();function s({status:t}){let{data:i,setData:s,post:c,processing:l,errors:u}=e({email:``});return(0,o.jsxs)(a,{children:[(0,o.jsx)(n,{title:`Forgot password`}),(0,o.jsxs)(`div`,{className:`mb-8 anim-down`,children:[(0,o.jsx)(`h1`,{className:`text-2xl font-bold text-[#1A2421] tracking-tight`,children:`Forgot password?`}),(0,o.jsx)(`p`,{className:`mt-1.5 text-sm text-gray-400 font-medium`,children:`Enter your email and we'll send a reset link.`})]}),t&&(0,o.jsx)(`div`,{className:`mb-6 px-4 py-3 rounded-xl bg-emerald-50 border border-emerald-100 text-sm font-semibold text-emerald-700 anim-fade`,children:t}),(0,o.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),c(route(`password.email`))},className:`anim-up`,style:{animationDelay:`0.1s`},children:[(0,o.jsxs)(`div`,{children:[(0,o.jsx)(`label`,{htmlFor:`email`,className:`block text-sm font-semibold text-gray-600 mb-1.5`,children:`Email address`}),(0,o.jsx)(`input`,{id:`email`,type:`email`,autoComplete:`email`,autoFocus:!0,value:i.email,onChange:e=>s(`email`,e.target.value),placeholder:`you@company.com`,className:`auth-input${u.email?` !border-red-300 !bg-red-50/50`:``}`}),u.email&&(0,o.jsx)(`p`,{className:`mt-1.5 text-xs font-semibold text-red-500`,children:u.email})]}),(0,o.jsx)(`button`,{type:`submit`,disabled:l,className:`mt-6 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed`,children:l?(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(`svg`,{className:`w-4 h-4 animate-spin text-white/60`,fill:`none`,viewBox:`0 0 24 24`,children:[(0,o.jsx)(`circle`,{className:`opacity-25`,cx:`12`,cy:`12`,r:`10`,stroke:`currentColor`,strokeWidth:`4`}),(0,o.jsx)(`path`,{className:`opacity-75`,fill:`currentColor`,d:`M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z`})]}),`Sending…`]}):`Send reset link`})]}),(0,o.jsx)(`p`,{className:`mt-7 text-center text-sm text-gray-400 font-medium anim-fade`,style:{animationDelay:`0.18s`},children:(0,o.jsx)(r,{href:route(`login`),className:`text-[#3D4E4B] font-semibold hover:text-[#D4A017] transition-colors duration-200`,children:`← Back to sign in`})})]})}export{s as default}; import{a as e,c as t,n,r,t as i}from"./app-CR33GSSe.js";import{t as a}from"./GuestLayout-RqG17kT8.js";t();var o=i();function s({status:t}){let{data:i,setData:s,post:c,processing:l,errors:u}=e({email:``});return(0,o.jsxs)(a,{children:[(0,o.jsx)(n,{title:`Forgot password`}),(0,o.jsxs)(`div`,{className:`mb-8 anim-down`,children:[(0,o.jsx)(`h1`,{className:`text-2xl font-bold text-[#1A2421] tracking-tight`,children:`Forgot password?`}),(0,o.jsx)(`p`,{className:`mt-1.5 text-sm text-gray-400 font-medium`,children:`Enter your email and we'll send a reset link.`})]}),t&&(0,o.jsx)(`div`,{className:`mb-6 px-4 py-3 rounded-xl bg-emerald-50 border border-emerald-100 text-sm font-semibold text-emerald-700 anim-fade`,children:t}),(0,o.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),c(route(`password.email`))},className:`anim-up`,style:{animationDelay:`0.1s`},children:[(0,o.jsxs)(`div`,{children:[(0,o.jsx)(`label`,{htmlFor:`email`,className:`block text-sm font-semibold text-gray-600 mb-1.5`,children:`Email address`}),(0,o.jsx)(`input`,{id:`email`,type:`email`,autoComplete:`email`,autoFocus:!0,value:i.email,onChange:e=>s(`email`,e.target.value),placeholder:`you@company.com`,className:`auth-input${u.email?` !border-red-300 !bg-red-50/50`:``}`}),u.email&&(0,o.jsx)(`p`,{className:`mt-1.5 text-xs font-semibold text-red-500`,children:u.email})]}),(0,o.jsx)(`button`,{type:`submit`,disabled:l,className:`mt-6 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed`,children:l?(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(`svg`,{className:`w-4 h-4 animate-spin text-white/60`,fill:`none`,viewBox:`0 0 24 24`,children:[(0,o.jsx)(`circle`,{className:`opacity-25`,cx:`12`,cy:`12`,r:`10`,stroke:`currentColor`,strokeWidth:`4`}),(0,o.jsx)(`path`,{className:`opacity-75`,fill:`currentColor`,d:`M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z`})]}),`Sending…`]}):`Send reset link`})]}),(0,o.jsx)(`p`,{className:`mt-7 text-center text-sm text-gray-400 font-medium anim-fade`,style:{animationDelay:`0.18s`},children:(0,o.jsx)(r,{href:route(`login`),className:`text-[#3D4E4B] font-semibold hover:text-[#D4A017] transition-colors duration-200`,children:`← Back to sign in`})})]})}export{s as default};
@@ -1 +1 @@
import{c as e,o as t,r as n,t as r}from"./app-BJ7g6sa8.js";e();var i=r();function a({children:e}){let{system_settings:r}=t().props,a=r?.app_name||`biiproject kit v2`,o=r?.app_logo,s=r?.app_logo_text||`BK`;return(0,i.jsxs)(`div`,{className:`min-h-screen flex font-sans`,children:[(0,i.jsxs)(`div`,{className:`hidden lg:flex lg:w-[44%] bg-[#3D4E4B] flex-col justify-between p-14 relative overflow-hidden shrink-0`,children:[(0,i.jsxs)(`svg`,{className:`absolute inset-0 w-full h-full pointer-events-none`,xmlns:`http://www.w3.org/2000/svg`,children:[(0,i.jsx)(`defs`,{children:(0,i.jsx)(`pattern`,{id:`dots`,x:`0`,y:`0`,width:`28`,height:`28`,patternUnits:`userSpaceOnUse`,children:(0,i.jsx)(`circle`,{cx:`1.5`,cy:`1.5`,r:`1.5`,fill:`white`,fillOpacity:`0.07`})})}),(0,i.jsx)(`rect`,{width:`100%`,height:`100%`,fill:`url(#dots)`})]}),(0,i.jsx)(`div`,{className:`absolute -bottom-40 -right-40 w-[480px] h-[480px] rounded-full border border-white/[0.06] pointer-events-none`}),(0,i.jsx)(`div`,{className:`absolute -bottom-64 -right-64 w-[700px] h-[700px] rounded-full border border-white/[0.04] pointer-events-none`}),(0,i.jsx)(`div`,{className:`absolute top-[-60px] left-[-60px] w-[300px] h-[300px] rounded-full border border-white/[0.04] pointer-events-none`}),(0,i.jsx)(`div`,{className:`relative z-10 anim-fade`,children:(0,i.jsxs)(n,{href:`/`,className:`inline-flex items-center gap-3 group`,children:[(0,i.jsx)(`div`,{className:`w-10 h-10 rounded-[0.75rem] flex items-center justify-center overflow-hidden text-sm font-bold text-white shrink-0 ${o?`bg-white/10`:`bg-[#D4A017]`}`,children:o?(0,i.jsx)(`img`,{src:o,alt:a,className:`w-full h-full object-contain`}):s}),(0,i.jsx)(`span`,{className:`text-white font-bold text-base tracking-tight`,children:a})]})}),(0,i.jsxs)(`div`,{className:`relative z-10 anim-up`,style:{animationDelay:`0.1s`},children:[(0,i.jsx)(`p`,{className:`text-[#D4A017] text-xs font-bold uppercase tracking-[0.18em] mb-5`,children:`Enterprise Platform`}),(0,i.jsxs)(`h2`,{className:`text-white text-[2rem] font-bold leading-[1.2] tracking-tight`,children:[`Manage your`,(0,i.jsx)(`br`,{}),`organization`,(0,i.jsx)(`br`,{}),`with precision.`]}),(0,i.jsx)(`p`,{className:`mt-5 text-[#E3EBE8]/45 text-sm leading-relaxed max-w-xs`,children:`Access control, user management, and system configuration — unified in one secure interface.`}),(0,i.jsx)(`div`,{className:`mt-9 flex flex-col gap-3`,children:[`Role-based access control`,`Real-time audit logs`,`Multi-level permissions`].map(e=>(0,i.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,i.jsx)(`div`,{className:`w-1.5 h-1.5 rounded-full bg-[#D4A017] shrink-0`}),(0,i.jsx)(`span`,{className:`text-[#E3EBE8]/55 text-sm font-medium`,children:e})]},e))})]}),(0,i.jsx)(`div`,{className:`relative z-10 anim-fade`,style:{animationDelay:`0.2s`},children:(0,i.jsxs)(`p`,{className:`text-[#E3EBE8]/25 text-xs`,children:[`© `,new Date().getFullYear(),` `,a,`. All rights reserved.`]})})]}),(0,i.jsxs)(`div`,{className:`flex-1 flex flex-col items-center justify-center bg-white px-8 py-12 min-h-screen`,children:[(0,i.jsxs)(`div`,{className:`lg:hidden mb-10 flex items-center gap-3 anim-down`,children:[(0,i.jsx)(`div`,{className:`w-9 h-9 rounded-[0.6rem] flex items-center justify-center text-sm font-bold overflow-hidden ${o?``:`bg-[#3D4E4B] text-white`}`,children:o?(0,i.jsx)(`img`,{src:o,alt:a,className:`w-full h-full object-contain`}):s}),(0,i.jsx)(`span`,{className:`text-[#3D4E4B] font-bold text-base tracking-tight`,children:a})]}),(0,i.jsx)(`div`,{className:`w-full max-w-[360px]`,children:e}),(0,i.jsx)(`div`,{className:`mt-10 anim-fade`,style:{animationDelay:`0.35s`},children:(0,i.jsxs)(n,{href:`/`,className:`inline-flex items-center gap-1.5 text-xs font-semibold text-gray-300 hover:text-[#3D4E4B] transition-colors duration-200 tracking-tight`,children:[(0,i.jsx)(`svg`,{className:`w-3.5 h-3.5`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2.5,children:(0,i.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M10 19l-7-7m0 0l7-7m-7 7h18`})}),`Back to home`]})})]})]})}export{a as t}; import{c as e,o as t,r as n,t as r}from"./app-CR33GSSe.js";e();var i=r();function a({children:e}){let{system_settings:r}=t().props,a=r?.app_name||`biiproject kit v2`,o=r?.app_logo,s=r?.app_logo_text||`BK`;return(0,i.jsxs)(`div`,{className:`min-h-screen flex font-sans`,children:[(0,i.jsxs)(`div`,{className:`hidden lg:flex lg:w-[44%] bg-[#3D4E4B] flex-col justify-between p-14 relative overflow-hidden shrink-0`,children:[(0,i.jsxs)(`svg`,{className:`absolute inset-0 w-full h-full pointer-events-none`,xmlns:`http://www.w3.org/2000/svg`,children:[(0,i.jsx)(`defs`,{children:(0,i.jsx)(`pattern`,{id:`dots`,x:`0`,y:`0`,width:`28`,height:`28`,patternUnits:`userSpaceOnUse`,children:(0,i.jsx)(`circle`,{cx:`1.5`,cy:`1.5`,r:`1.5`,fill:`white`,fillOpacity:`0.07`})})}),(0,i.jsx)(`rect`,{width:`100%`,height:`100%`,fill:`url(#dots)`})]}),(0,i.jsx)(`div`,{className:`absolute -bottom-40 -right-40 w-[480px] h-[480px] rounded-full border border-white/[0.06] pointer-events-none`}),(0,i.jsx)(`div`,{className:`absolute -bottom-64 -right-64 w-[700px] h-[700px] rounded-full border border-white/[0.04] pointer-events-none`}),(0,i.jsx)(`div`,{className:`absolute top-[-60px] left-[-60px] w-[300px] h-[300px] rounded-full border border-white/[0.04] pointer-events-none`}),(0,i.jsx)(`div`,{className:`relative z-10 anim-fade`,children:(0,i.jsxs)(n,{href:`/`,className:`inline-flex items-center gap-3 group`,children:[(0,i.jsx)(`div`,{className:`w-10 h-10 rounded-[0.75rem] flex items-center justify-center overflow-hidden text-sm font-bold text-white shrink-0 ${o?`bg-white/10`:`bg-[#D4A017]`}`,children:o?(0,i.jsx)(`img`,{src:o,alt:a,className:`w-full h-full object-contain`}):s}),(0,i.jsx)(`span`,{className:`text-white font-bold text-base tracking-tight`,children:a})]})}),(0,i.jsxs)(`div`,{className:`relative z-10 anim-up`,style:{animationDelay:`0.1s`},children:[(0,i.jsx)(`p`,{className:`text-[#D4A017] text-xs font-bold uppercase tracking-[0.18em] mb-5`,children:`Enterprise Platform`}),(0,i.jsxs)(`h2`,{className:`text-white text-[2rem] font-bold leading-[1.2] tracking-tight`,children:[`Manage your`,(0,i.jsx)(`br`,{}),`organization`,(0,i.jsx)(`br`,{}),`with precision.`]}),(0,i.jsx)(`p`,{className:`mt-5 text-[#E3EBE8]/45 text-sm leading-relaxed max-w-xs`,children:`Access control, user management, and system configuration — unified in one secure interface.`}),(0,i.jsx)(`div`,{className:`mt-9 flex flex-col gap-3`,children:[`Role-based access control`,`Real-time audit logs`,`Multi-level permissions`].map(e=>(0,i.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,i.jsx)(`div`,{className:`w-1.5 h-1.5 rounded-full bg-[#D4A017] shrink-0`}),(0,i.jsx)(`span`,{className:`text-[#E3EBE8]/55 text-sm font-medium`,children:e})]},e))})]}),(0,i.jsx)(`div`,{className:`relative z-10 anim-fade`,style:{animationDelay:`0.2s`},children:(0,i.jsxs)(`p`,{className:`text-[#E3EBE8]/25 text-xs`,children:[`© `,new Date().getFullYear(),` `,a,`. All rights reserved.`]})})]}),(0,i.jsxs)(`div`,{className:`flex-1 flex flex-col items-center justify-center bg-white px-8 py-12 min-h-screen`,children:[(0,i.jsxs)(`div`,{className:`lg:hidden mb-10 flex items-center gap-3 anim-down`,children:[(0,i.jsx)(`div`,{className:`w-9 h-9 rounded-[0.6rem] flex items-center justify-center text-sm font-bold overflow-hidden ${o?``:`bg-[#3D4E4B] text-white`}`,children:o?(0,i.jsx)(`img`,{src:o,alt:a,className:`w-full h-full object-contain`}):s}),(0,i.jsx)(`span`,{className:`text-[#3D4E4B] font-bold text-base tracking-tight`,children:a})]}),(0,i.jsx)(`div`,{className:`w-full max-w-[360px]`,children:e}),(0,i.jsx)(`div`,{className:`mt-10 anim-fade`,style:{animationDelay:`0.35s`},children:(0,i.jsxs)(n,{href:`/`,className:`inline-flex items-center gap-1.5 text-xs font-semibold text-gray-300 hover:text-[#3D4E4B] transition-colors duration-200 tracking-tight`,children:[(0,i.jsx)(`svg`,{className:`w-3.5 h-3.5`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2.5,children:(0,i.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M10 19l-7-7m0 0l7-7m-7 7h18`})}),`Back to home`]})})]})]})}export{a as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{d as e,s as t}from"./app-BJ7g6sa8.js";var n=e(t(),1);function r({children:e}){return(0,n.createPortal)(e,document.body)}export{r as t}; import{d as e,s as t}from"./app-CR33GSSe.js";var n=e(t(),1);function r({children:e}){return(0,n.createPortal)(e,document.body)}export{r as t};
@@ -1 +1 @@
import{t as e}from"./app-BJ7g6sa8.js";var t=e();function n({className:e=``,disabled:n,children:r,...i}){return(0,t.jsx)(`button`,{...i,className:`inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-bold tracking-tight text-white transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 active:bg-gray-900 ${n&&`opacity-25`} `+e,disabled:n,children:r})}export{n as t}; import{t as e}from"./app-CR33GSSe.js";var t=e();function n({className:e=``,disabled:n,children:r,...i}){return(0,t.jsx)(`button`,{...i,className:`inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-bold tracking-tight text-white transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 active:bg-gray-900 ${n&&`opacity-25`} `+e,disabled:n,children:r})}export{n as t};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{a as e,c as t,n,t as r}from"./app-BJ7g6sa8.js";import{t as i}from"./GuestLayout-CN-YY0cs.js";t();var a=r();function o({token:t,email:r}){let{data:o,setData:s,post:c,processing:l,errors:u,reset:d}=e({token:t,email:r,password:``,password_confirmation:``});return(0,a.jsxs)(i,{children:[(0,a.jsx)(n,{title:`Reset password`}),(0,a.jsxs)(`div`,{className:`mb-8 anim-down`,children:[(0,a.jsx)(`h1`,{className:`text-2xl font-bold text-[#1A2421] tracking-tight`,children:`Set new password`}),(0,a.jsxs)(`p`,{className:`mt-1.5 text-sm text-gray-400 font-medium`,children:[`Choose a strong password for `,(0,a.jsx)(`span`,{className:`text-[#3D4E4B] font-semibold`,children:r}),`.`]})]}),(0,a.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),c(route(`password.store`),{onFinish:()=>d(`password`,`password_confirmation`)})},className:`space-y-4 anim-up`,style:{animationDelay:`0.1s`},children:[(0,a.jsx)(`input`,{type:`hidden`,value:o.email}),(0,a.jsx)(`input`,{type:`hidden`,value:o.token}),(0,a.jsxs)(`div`,{children:[(0,a.jsx)(`label`,{htmlFor:`password`,className:`block text-sm font-semibold text-gray-600 mb-1.5`,children:`New password`}),(0,a.jsx)(`input`,{id:`password`,type:`password`,autoComplete:`new-password`,autoFocus:!0,value:o.password,onChange:e=>s(`password`,e.target.value),placeholder:`Min. 8 characters`,className:`auth-input${u.password?` !border-red-300 !bg-red-50/50`:``}`}),u.password&&(0,a.jsx)(`p`,{className:`mt-1.5 text-xs font-semibold text-red-500`,children:u.password})]}),(0,a.jsxs)(`div`,{children:[(0,a.jsx)(`label`,{htmlFor:`password_confirmation`,className:`block text-sm font-semibold text-gray-600 mb-1.5`,children:`Confirm new password`}),(0,a.jsx)(`input`,{id:`password_confirmation`,type:`password`,autoComplete:`new-password`,value:o.password_confirmation,onChange:e=>s(`password_confirmation`,e.target.value),placeholder:`••••••••`,className:`auth-input${u.password_confirmation?` !border-red-300 !bg-red-50/50`:``}`}),u.password_confirmation&&(0,a.jsx)(`p`,{className:`mt-1.5 text-xs font-semibold text-red-500`,children:u.password_confirmation})]}),(0,a.jsx)(`button`,{type:`submit`,disabled:l,className:`mt-2 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed`,children:l?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)(`svg`,{className:`w-4 h-4 animate-spin text-white/60`,fill:`none`,viewBox:`0 0 24 24`,children:[(0,a.jsx)(`circle`,{className:`opacity-25`,cx:`12`,cy:`12`,r:`10`,stroke:`currentColor`,strokeWidth:`4`}),(0,a.jsx)(`path`,{className:`opacity-75`,fill:`currentColor`,d:`M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z`})]}),`Saving…`]}):`Reset password`})]})]})}export{o as default}; import{a as e,c as t,n,t as r}from"./app-CR33GSSe.js";import{t as i}from"./GuestLayout-RqG17kT8.js";t();var a=r();function o({token:t,email:r}){let{data:o,setData:s,post:c,processing:l,errors:u,reset:d}=e({token:t,email:r,password:``,password_confirmation:``});return(0,a.jsxs)(i,{children:[(0,a.jsx)(n,{title:`Reset password`}),(0,a.jsxs)(`div`,{className:`mb-8 anim-down`,children:[(0,a.jsx)(`h1`,{className:`text-2xl font-bold text-[#1A2421] tracking-tight`,children:`Set new password`}),(0,a.jsxs)(`p`,{className:`mt-1.5 text-sm text-gray-400 font-medium`,children:[`Choose a strong password for `,(0,a.jsx)(`span`,{className:`text-[#3D4E4B] font-semibold`,children:r}),`.`]})]}),(0,a.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),c(route(`password.store`),{onFinish:()=>d(`password`,`password_confirmation`)})},className:`space-y-4 anim-up`,style:{animationDelay:`0.1s`},children:[(0,a.jsx)(`input`,{type:`hidden`,value:o.email}),(0,a.jsx)(`input`,{type:`hidden`,value:o.token}),(0,a.jsxs)(`div`,{children:[(0,a.jsx)(`label`,{htmlFor:`password`,className:`block text-sm font-semibold text-gray-600 mb-1.5`,children:`New password`}),(0,a.jsx)(`input`,{id:`password`,type:`password`,autoComplete:`new-password`,autoFocus:!0,value:o.password,onChange:e=>s(`password`,e.target.value),placeholder:`Min. 8 characters`,className:`auth-input${u.password?` !border-red-300 !bg-red-50/50`:``}`}),u.password&&(0,a.jsx)(`p`,{className:`mt-1.5 text-xs font-semibold text-red-500`,children:u.password})]}),(0,a.jsxs)(`div`,{children:[(0,a.jsx)(`label`,{htmlFor:`password_confirmation`,className:`block text-sm font-semibold text-gray-600 mb-1.5`,children:`Confirm new password`}),(0,a.jsx)(`input`,{id:`password_confirmation`,type:`password`,autoComplete:`new-password`,value:o.password_confirmation,onChange:e=>s(`password_confirmation`,e.target.value),placeholder:`••••••••`,className:`auth-input${u.password_confirmation?` !border-red-300 !bg-red-50/50`:``}`}),u.password_confirmation&&(0,a.jsx)(`p`,{className:`mt-1.5 text-xs font-semibold text-red-500`,children:u.password_confirmation})]}),(0,a.jsx)(`button`,{type:`submit`,disabled:l,className:`mt-2 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed`,children:l?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)(`svg`,{className:`w-4 h-4 animate-spin text-white/60`,fill:`none`,viewBox:`0 0 24 24`,children:[(0,a.jsx)(`circle`,{className:`opacity-25`,cx:`12`,cy:`12`,r:`10`,stroke:`currentColor`,strokeWidth:`4`}),(0,a.jsx)(`path`,{className:`opacity-75`,fill:`currentColor`,d:`M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z`})]}),`Saving…`]}):`Reset password`})]})]})}export{o as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as e,d as t,t as n}from"./app-BJ7g6sa8.js";var r=n();function i({message:e,className:t=``,...n}){return e?(0,r.jsx)(`p`,{...n,className:`text-sm text-red-600 `+t,children:e}):null}function a({value:e,className:t=``,children:n,...i}){return(0,r.jsx)(`label`,{...i,className:`block text-sm font-medium text-gray-700 `+t,children:e||n})}var o=t(e(),1),s=Object.defineProperty,c=(e,t,n)=>t in e?s(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,l=(e,t,n)=>(c(e,typeof t==`symbol`?t:t+``,n),n),u=new class{constructor(){l(this,`current`,this.detect()),l(this,`handoffState`,`pending`),l(this,`currentId`,0)}set(e){this.current!==e&&(this.handoffState=`pending`,this.currentId=0,this.current=e)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return this.current===`server`}get isClient(){return this.current===`client`}detect(){return typeof window>`u`||typeof document>`u`?`server`:`client`}handoff(){this.handoffState===`pending`&&(this.handoffState=`complete`)}get isHandoffComplete(){return this.handoffState===`complete`}};function d(e){typeof queueMicrotask==`function`?queueMicrotask(e):Promise.resolve().then(e).catch(e=>setTimeout(()=>{throw e}))}function f(){let e=[],t={addEventListener(e,n,r,i){return e.addEventListener(n,r,i),t.add(()=>e.removeEventListener(n,r,i))},requestAnimationFrame(...e){let n=requestAnimationFrame(...e);return t.add(()=>cancelAnimationFrame(n))},nextFrame(...e){return t.requestAnimationFrame(()=>t.requestAnimationFrame(...e))},setTimeout(...e){let n=setTimeout(...e);return t.add(()=>clearTimeout(n))},microTask(...e){let n={current:!0};return d(()=>{n.current&&e[0]()}),t.add(()=>{n.current=!1})},style(e,t,n){let r=e.style.getPropertyValue(t);return Object.assign(e.style,{[t]:n}),this.add(()=>{Object.assign(e.style,{[t]:r})})},group(e){let t=f();return e(t),this.add(()=>t.dispose())},add(t){return e.includes(t)||e.push(t),()=>{let n=e.indexOf(t);if(n>=0)for(let t of e.splice(n,1))t()}},dispose(){for(let t of e.splice(0))t()}};return t}function p(){let[e]=(0,o.useState)(f);return(0,o.useEffect)(()=>()=>e.dispose(),[e]),e}var m=(e,t)=>{u.isServer?(0,o.useEffect)(e,t):(0,o.useLayoutEffect)(e,t)};function h(e){let t=(0,o.useRef)(e);return m(()=>{t.current=e},[e]),t}var g=function(e){let t=h(e);return o.useCallback((...e)=>t.current(...e),[t])};function _(...e){return Array.from(new Set(e.flatMap(e=>typeof e==`string`?e.split(` `):[]))).filter(Boolean).join(` `)}function v(e,t,...n){if(e in t){let r=t[e];return typeof r==`function`?r(...n):r}let r=Error(`Tried to handle "${e}" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map(e=>`"${e}"`).join(`, `)}.`);throw Error.captureStackTrace&&Error.captureStackTrace(r,v),r}var y=(e=>(e[e.None=0]=`None`,e[e.RenderStrategy=1]=`RenderStrategy`,e[e.Static=2]=`Static`,e))(y||{}),b=(e=>(e[e.Unmount=0]=`Unmount`,e[e.Hidden=1]=`Hidden`,e))(b||{});function x(){let e=ee();return(0,o.useCallback)(t=>S({mergeRefs:e,...t}),[e])}function S({ourProps:e,theirProps:t,slot:n,defaultTag:r,features:i,visible:a=!0,name:o,mergeRefs:s}){s??=w;let c=T(t,e);if(a)return C(c,n,r,o,s);let l=i??0;if(l&2){let{static:e=!1,...t}=c;if(e)return C(t,n,r,o,s)}if(l&1){let{unmount:e=!0,...t}=c;return v(+!e,{0(){return null},1(){return C({...t,hidden:!0,style:{display:`none`}},n,r,o,s)}})}return C(c,n,r,o,s)}function C(e,t={},n,r,i){let{as:a=n,children:s,refName:c=`ref`,...l}=O(e,[`unmount`,`static`]),u=e.ref===void 0?{}:{[c]:e.ref},d=typeof s==`function`?s(t):s;d=A(d),`className`in l&&l.className&&typeof l.className==`function`&&(l.className=l.className(t)),l[`aria-labelledby`]&&l[`aria-labelledby`]===l.id&&(l[`aria-labelledby`]=void 0);let f={};if(t){let e=!1,n=[];for(let[r,i]of Object.entries(t))typeof i==`boolean`&&(e=!0),i===!0&&n.push(r.replace(/([A-Z])/g,e=>`-${e.toLowerCase()}`));if(e){f[`data-headlessui-state`]=n.join(` `);for(let e of n)f[`data-${e}`]=``}}if(j(a)&&(Object.keys(D(l)).length>0||Object.keys(D(f)).length>0))if(!(0,o.isValidElement)(d)||Array.isArray(d)&&d.length>1||M(d)){if(Object.keys(D(l)).length>0)throw Error([`Passing props on "Fragment"!`,``,`The current component <${r} /> is rendering a "Fragment".`,`However we need to passthrough the following props:`,Object.keys(D(l)).concat(Object.keys(D(f))).map(e=>` - ${e}`).join(` import{c as e,d as t,t as n}from"./app-CR33GSSe.js";var r=n();function i({message:e,className:t=``,...n}){return e?(0,r.jsx)(`p`,{...n,className:`text-sm text-red-600 `+t,children:e}):null}function a({value:e,className:t=``,children:n,...i}){return(0,r.jsx)(`label`,{...i,className:`block text-sm font-medium text-gray-700 `+t,children:e||n})}var o=t(e(),1),s=Object.defineProperty,c=(e,t,n)=>t in e?s(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,l=(e,t,n)=>(c(e,typeof t==`symbol`?t:t+``,n),n),u=new class{constructor(){l(this,`current`,this.detect()),l(this,`handoffState`,`pending`),l(this,`currentId`,0)}set(e){this.current!==e&&(this.handoffState=`pending`,this.currentId=0,this.current=e)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return this.current===`server`}get isClient(){return this.current===`client`}detect(){return typeof window>`u`||typeof document>`u`?`server`:`client`}handoff(){this.handoffState===`pending`&&(this.handoffState=`complete`)}get isHandoffComplete(){return this.handoffState===`complete`}};function d(e){typeof queueMicrotask==`function`?queueMicrotask(e):Promise.resolve().then(e).catch(e=>setTimeout(()=>{throw e}))}function f(){let e=[],t={addEventListener(e,n,r,i){return e.addEventListener(n,r,i),t.add(()=>e.removeEventListener(n,r,i))},requestAnimationFrame(...e){let n=requestAnimationFrame(...e);return t.add(()=>cancelAnimationFrame(n))},nextFrame(...e){return t.requestAnimationFrame(()=>t.requestAnimationFrame(...e))},setTimeout(...e){let n=setTimeout(...e);return t.add(()=>clearTimeout(n))},microTask(...e){let n={current:!0};return d(()=>{n.current&&e[0]()}),t.add(()=>{n.current=!1})},style(e,t,n){let r=e.style.getPropertyValue(t);return Object.assign(e.style,{[t]:n}),this.add(()=>{Object.assign(e.style,{[t]:r})})},group(e){let t=f();return e(t),this.add(()=>t.dispose())},add(t){return e.includes(t)||e.push(t),()=>{let n=e.indexOf(t);if(n>=0)for(let t of e.splice(n,1))t()}},dispose(){for(let t of e.splice(0))t()}};return t}function p(){let[e]=(0,o.useState)(f);return(0,o.useEffect)(()=>()=>e.dispose(),[e]),e}var m=(e,t)=>{u.isServer?(0,o.useEffect)(e,t):(0,o.useLayoutEffect)(e,t)};function h(e){let t=(0,o.useRef)(e);return m(()=>{t.current=e},[e]),t}var g=function(e){let t=h(e);return o.useCallback((...e)=>t.current(...e),[t])};function _(...e){return Array.from(new Set(e.flatMap(e=>typeof e==`string`?e.split(` `):[]))).filter(Boolean).join(` `)}function v(e,t,...n){if(e in t){let r=t[e];return typeof r==`function`?r(...n):r}let r=Error(`Tried to handle "${e}" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map(e=>`"${e}"`).join(`, `)}.`);throw Error.captureStackTrace&&Error.captureStackTrace(r,v),r}var y=(e=>(e[e.None=0]=`None`,e[e.RenderStrategy=1]=`RenderStrategy`,e[e.Static=2]=`Static`,e))(y||{}),b=(e=>(e[e.Unmount=0]=`Unmount`,e[e.Hidden=1]=`Hidden`,e))(b||{});function x(){let e=ee();return(0,o.useCallback)(t=>S({mergeRefs:e,...t}),[e])}function S({ourProps:e,theirProps:t,slot:n,defaultTag:r,features:i,visible:a=!0,name:o,mergeRefs:s}){s??=w;let c=T(t,e);if(a)return C(c,n,r,o,s);let l=i??0;if(l&2){let{static:e=!1,...t}=c;if(e)return C(t,n,r,o,s)}if(l&1){let{unmount:e=!0,...t}=c;return v(+!e,{0(){return null},1(){return C({...t,hidden:!0,style:{display:`none`}},n,r,o,s)}})}return C(c,n,r,o,s)}function C(e,t={},n,r,i){let{as:a=n,children:s,refName:c=`ref`,...l}=O(e,[`unmount`,`static`]),u=e.ref===void 0?{}:{[c]:e.ref},d=typeof s==`function`?s(t):s;d=A(d),`className`in l&&l.className&&typeof l.className==`function`&&(l.className=l.className(t)),l[`aria-labelledby`]&&l[`aria-labelledby`]===l.id&&(l[`aria-labelledby`]=void 0);let f={};if(t){let e=!1,n=[];for(let[r,i]of Object.entries(t))typeof i==`boolean`&&(e=!0),i===!0&&n.push(r.replace(/([A-Z])/g,e=>`-${e.toLowerCase()}`));if(e){f[`data-headlessui-state`]=n.join(` `);for(let e of n)f[`data-${e}`]=``}}if(j(a)&&(Object.keys(D(l)).length>0||Object.keys(D(f)).length>0))if(!(0,o.isValidElement)(d)||Array.isArray(d)&&d.length>1||M(d)){if(Object.keys(D(l)).length>0)throw Error([`Passing props on "Fragment"!`,``,`The current component <${r} /> is rendering a "Fragment".`,`However we need to passthrough the following props:`,Object.keys(D(l)).concat(Object.keys(D(f))).map(e=>` - ${e}`).join(`
`),``,`You can apply a few solutions:`,['Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".',`Render a single element as the child so that we can forward the props onto that element.`].map(e=>` - ${e}`).join(` `),``,`You can apply a few solutions:`,['Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".',`Render a single element as the child so that we can forward the props onto that element.`].map(e=>` - ${e}`).join(`
`)].join(` `)].join(`
`))}else{let e=d.props?.className,t=typeof e==`function`?(...t)=>_(e(...t),l.className):_(e,l.className),n=t?{className:t}:{},r=T(d.props,D(O(l,[`ref`])));for(let e in f)e in r&&delete f[e];return(0,o.cloneElement)(d,Object.assign({},r,f,u,{ref:i(k(d),u.ref)},n))}return(0,o.createElement)(a,Object.assign({},O(l,[`ref`]),!j(a)&&u,!j(a)&&f),d)}function ee(){let e=(0,o.useRef)([]),t=(0,o.useCallback)(t=>{for(let n of e.current)n!=null&&(typeof n==`function`?n(t):n.current=t)},[]);return(...n)=>{if(!n.every(e=>e==null))return e.current=n,t}}function w(...e){return e.every(e=>e==null)?void 0:t=>{for(let n of e)n!=null&&(typeof n==`function`?n(t):n.current=t)}}function T(...e){if(e.length===0)return{};if(e.length===1)return e[0];let t={},n={};for(let r of e)for(let e in r)e.startsWith(`on`)&&typeof r[e]==`function`?(n[e]??(n[e]=[]),n[e].push(r[e])):t[e]=r[e];if(t.disabled||t[`aria-disabled`])for(let e in n)/^(on(?:Click|Pointer|Mouse|Key)(?:Down|Up|Press)?)$/.test(e)&&(n[e]=[e=>(e?.preventDefault)?.call(e)]);for(let e in n)Object.assign(t,{[e](t,...r){let i=n[e];for(let e of i){if((t instanceof Event||t?.nativeEvent instanceof Event)&&t.defaultPrevented)return;e(t,...r)}}});return t}function E(e){return Object.assign((0,o.forwardRef)(e),{displayName:e.displayName??e.name})}function D(e){let t=Object.assign({},e);for(let e in t)t[e]===void 0&&delete t[e];return t}function O(e,t=[]){let n=Object.assign({},e);for(let e of t)e in n&&delete n[e];return n}function k(e){return`18.3.1`.split(`.`)[0]>=`19`?e.props.ref:e.ref}function A(e){if(e!=null&&e.$$typeof===Symbol.for(`react.lazy`)){let t=e._payload;if(t!=null&&t.status===`fulfilled`)return A(t.value)}return e}function j(e){return e===o.Fragment||e===Symbol.for(`react.fragment`)}function M(e){return j(e.type)}var N=Symbol();function P(e,t=!0){return Object.assign(e,{[N]:t})}function F(...e){let t=(0,o.useRef)(e);(0,o.useEffect)(()=>{t.current=e},[e]);let n=g(e=>{for(let n of t.current)n!=null&&(typeof n==`function`?n(e):n.current=e)});return e.every(e=>e==null||e?.[N])?void 0:n}function I(e=0){let[t,n]=(0,o.useState)(e);return{flags:t,setFlag:(0,o.useCallback)(e=>n(e),[]),addFlag:(0,o.useCallback)(e=>n(t=>t|e),[]),hasFlag:(0,o.useCallback)(e=>(t&e)===e,[t]),removeFlag:(0,o.useCallback)(e=>n(t=>t&~e),[]),toggleFlag:(0,o.useCallback)(e=>n(t=>t^e),[])}}typeof process<`u`&&typeof globalThis<`u`&&typeof Element<`u`&&(process==null?void 0:{})?.NODE_ENV===`test`&&(Element==null?void 0:Element.prototype)?.getAnimations===void 0&&(Element.prototype.getAnimations=function(){return console.warn(["Headless UI has polyfilled `Element.prototype.getAnimations` for your tests.","Please install a proper polyfill e.g. `jsdom-testing-mocks`, to silence these warnings.",``,`Example usage:`,"```js",`import { mockAnimationsApi } from 'jsdom-testing-mocks'`,`mockAnimationsApi()`,"```"].join(` `))}else{let e=d.props?.className,t=typeof e==`function`?(...t)=>_(e(...t),l.className):_(e,l.className),n=t?{className:t}:{},r=T(d.props,D(O(l,[`ref`])));for(let e in f)e in r&&delete f[e];return(0,o.cloneElement)(d,Object.assign({},r,f,u,{ref:i(k(d),u.ref)},n))}return(0,o.createElement)(a,Object.assign({},O(l,[`ref`]),!j(a)&&u,!j(a)&&f),d)}function ee(){let e=(0,o.useRef)([]),t=(0,o.useCallback)(t=>{for(let n of e.current)n!=null&&(typeof n==`function`?n(t):n.current=t)},[]);return(...n)=>{if(!n.every(e=>e==null))return e.current=n,t}}function w(...e){return e.every(e=>e==null)?void 0:t=>{for(let n of e)n!=null&&(typeof n==`function`?n(t):n.current=t)}}function T(...e){if(e.length===0)return{};if(e.length===1)return e[0];let t={},n={};for(let r of e)for(let e in r)e.startsWith(`on`)&&typeof r[e]==`function`?(n[e]??(n[e]=[]),n[e].push(r[e])):t[e]=r[e];if(t.disabled||t[`aria-disabled`])for(let e in n)/^(on(?:Click|Pointer|Mouse|Key)(?:Down|Up|Press)?)$/.test(e)&&(n[e]=[e=>(e?.preventDefault)?.call(e)]);for(let e in n)Object.assign(t,{[e](t,...r){let i=n[e];for(let e of i){if((t instanceof Event||t?.nativeEvent instanceof Event)&&t.defaultPrevented)return;e(t,...r)}}});return t}function E(e){return Object.assign((0,o.forwardRef)(e),{displayName:e.displayName??e.name})}function D(e){let t=Object.assign({},e);for(let e in t)t[e]===void 0&&delete t[e];return t}function O(e,t=[]){let n=Object.assign({},e);for(let e of t)e in n&&delete n[e];return n}function k(e){return`18.3.1`.split(`.`)[0]>=`19`?e.props.ref:e.ref}function A(e){if(e!=null&&e.$$typeof===Symbol.for(`react.lazy`)){let t=e._payload;if(t!=null&&t.status===`fulfilled`)return A(t.value)}return e}function j(e){return e===o.Fragment||e===Symbol.for(`react.fragment`)}function M(e){return j(e.type)}var N=Symbol();function P(e,t=!0){return Object.assign(e,{[N]:t})}function F(...e){let t=(0,o.useRef)(e);(0,o.useEffect)(()=>{t.current=e},[e]);let n=g(e=>{for(let n of t.current)n!=null&&(typeof n==`function`?n(e):n.current=e)});return e.every(e=>e==null||e?.[N])?void 0:n}function I(e=0){let[t,n]=(0,o.useState)(e);return{flags:t,setFlag:(0,o.useCallback)(e=>n(e),[]),addFlag:(0,o.useCallback)(e=>n(t=>t|e),[]),hasFlag:(0,o.useCallback)(e=>(t&e)===e,[t]),removeFlag:(0,o.useCallback)(e=>n(t=>t&~e),[]),toggleFlag:(0,o.useCallback)(e=>n(t=>t^e),[])}}typeof process<`u`&&typeof globalThis<`u`&&typeof Element<`u`&&(process==null?void 0:{})?.NODE_ENV===`test`&&(Element==null?void 0:Element.prototype)?.getAnimations===void 0&&(Element.prototype.getAnimations=function(){return console.warn(["Headless UI has polyfilled `Element.prototype.getAnimations` for your tests.","Please install a proper polyfill e.g. `jsdom-testing-mocks`, to silence these warnings.",``,`Example usage:`,"```js",`import { mockAnimationsApi } from 'jsdom-testing-mocks'`,`mockAnimationsApi()`,"```"].join(`
@@ -1 +1 @@
import{a as e,c as t,d as n,t as r}from"./app-BJ7g6sa8.js";import{C as i,S as a,n as o,t as s}from"./TextInput-DV7QeRn3.js";import{t as c}from"./PrimaryButton-KeVcwQeg.js";var l=n(t(),1),u=r();function d({className:t=``}){let n=(0,l.useRef)(),r=(0,l.useRef)(),{data:d,setData:f,errors:p,put:m,reset:h,processing:g,recentlySuccessful:_}=e({current_password:``,password:``,password_confirmation:``});return(0,u.jsxs)(`section`,{className:t,children:[(0,u.jsxs)(`header`,{children:[(0,u.jsx)(`h2`,{className:`text-lg font-medium text-gray-900`,children:`Update Password`}),(0,u.jsx)(`p`,{className:`mt-1 text-sm text-gray-600`,children:`Ensure your account is using a long, random password to stay secure.`})]}),(0,u.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),m(route(`password.update`),{preserveScroll:!0,onSuccess:()=>h(),onError:e=>{e.password&&(h(`password`,`password_confirmation`),n.current.focus()),e.current_password&&(h(`current_password`),r.current.focus())}})},className:`mt-6 space-y-6`,children:[(0,u.jsxs)(`div`,{children:[(0,u.jsx)(a,{htmlFor:`current_password`,value:`Current Password`}),(0,u.jsx)(s,{id:`current_password`,ref:r,value:d.current_password,onChange:e=>f(`current_password`,e.target.value),type:`password`,className:`mt-1 block w-full`,autoComplete:`current-password`}),(0,u.jsx)(i,{message:p.current_password,className:`mt-2`})]}),(0,u.jsxs)(`div`,{children:[(0,u.jsx)(a,{htmlFor:`password`,value:`New Password`}),(0,u.jsx)(s,{id:`password`,ref:n,value:d.password,onChange:e=>f(`password`,e.target.value),type:`password`,className:`mt-1 block w-full`,autoComplete:`new-password`}),(0,u.jsx)(i,{message:p.password,className:`mt-2`})]}),(0,u.jsxs)(`div`,{children:[(0,u.jsx)(a,{htmlFor:`password_confirmation`,value:`Confirm Password`}),(0,u.jsx)(s,{id:`password_confirmation`,value:d.password_confirmation,onChange:e=>f(`password_confirmation`,e.target.value),type:`password`,className:`mt-1 block w-full`,autoComplete:`new-password`}),(0,u.jsx)(i,{message:p.password_confirmation,className:`mt-2`})]}),(0,u.jsxs)(`div`,{className:`flex items-center gap-4`,children:[(0,u.jsx)(c,{disabled:g,children:`Save`}),(0,u.jsx)(o,{show:_,enter:`transition ease-in-out`,enterFrom:`opacity-0`,leave:`transition ease-in-out`,leaveTo:`opacity-0`,children:(0,u.jsx)(`p`,{className:`text-sm text-gray-600`,children:`Saved.`})})]})]})]})}export{d as default}; import{a as e,c as t,d as n,t as r}from"./app-CR33GSSe.js";import{C as i,S as a,n as o,t as s}from"./TextInput-Ci82coVx.js";import{t as c}from"./PrimaryButton-DSkvm77Z.js";var l=n(t(),1),u=r();function d({className:t=``}){let n=(0,l.useRef)(),r=(0,l.useRef)(),{data:d,setData:f,errors:p,put:m,reset:h,processing:g,recentlySuccessful:_}=e({current_password:``,password:``,password_confirmation:``});return(0,u.jsxs)(`section`,{className:t,children:[(0,u.jsxs)(`header`,{children:[(0,u.jsx)(`h2`,{className:`text-lg font-medium text-gray-900`,children:`Update Password`}),(0,u.jsx)(`p`,{className:`mt-1 text-sm text-gray-600`,children:`Ensure your account is using a long, random password to stay secure.`})]}),(0,u.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),m(route(`password.update`),{preserveScroll:!0,onSuccess:()=>h(),onError:e=>{e.password&&(h(`password`,`password_confirmation`),n.current.focus()),e.current_password&&(h(`current_password`),r.current.focus())}})},className:`mt-6 space-y-6`,children:[(0,u.jsxs)(`div`,{children:[(0,u.jsx)(a,{htmlFor:`current_password`,value:`Current Password`}),(0,u.jsx)(s,{id:`current_password`,ref:r,value:d.current_password,onChange:e=>f(`current_password`,e.target.value),type:`password`,className:`mt-1 block w-full`,autoComplete:`current-password`}),(0,u.jsx)(i,{message:p.current_password,className:`mt-2`})]}),(0,u.jsxs)(`div`,{children:[(0,u.jsx)(a,{htmlFor:`password`,value:`New Password`}),(0,u.jsx)(s,{id:`password`,ref:n,value:d.password,onChange:e=>f(`password`,e.target.value),type:`password`,className:`mt-1 block w-full`,autoComplete:`new-password`}),(0,u.jsx)(i,{message:p.password,className:`mt-2`})]}),(0,u.jsxs)(`div`,{children:[(0,u.jsx)(a,{htmlFor:`password_confirmation`,value:`Confirm Password`}),(0,u.jsx)(s,{id:`password_confirmation`,value:d.password_confirmation,onChange:e=>f(`password_confirmation`,e.target.value),type:`password`,className:`mt-1 block w-full`,autoComplete:`new-password`}),(0,u.jsx)(i,{message:p.password_confirmation,className:`mt-2`})]}),(0,u.jsxs)(`div`,{className:`flex items-center gap-4`,children:[(0,u.jsx)(c,{disabled:g,children:`Save`}),(0,u.jsx)(o,{show:_,enter:`transition ease-in-out`,enterFrom:`opacity-0`,leave:`transition ease-in-out`,leaveTo:`opacity-0`,children:(0,u.jsx)(`p`,{className:`text-sm text-gray-600`,children:`Saved.`})})]})]})]})}export{d as default};
@@ -1 +1 @@
import{a as e,o as t,r as n,t as r}from"./app-BJ7g6sa8.js";import{C as i,S as a,n as o,t as s}from"./TextInput-DV7QeRn3.js";import{t as c}from"./PrimaryButton-KeVcwQeg.js";var l=r();function u({mustVerifyEmail:r,status:u,className:d=``}){let f=t().props.auth.user,{data:p,setData:m,patch:h,errors:g,processing:_,recentlySuccessful:v}=e({name:f.name,email:f.email});return(0,l.jsxs)(`section`,{className:d,children:[(0,l.jsxs)(`header`,{children:[(0,l.jsx)(`h2`,{className:`text-lg font-medium text-gray-900`,children:`Profile Information`}),(0,l.jsx)(`p`,{className:`mt-1 text-sm text-gray-600`,children:`Update your account's profile information and email address.`})]}),(0,l.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),h(route(`profile.update`))},className:`mt-6 space-y-6`,children:[(0,l.jsxs)(`div`,{children:[(0,l.jsx)(a,{htmlFor:`name`,value:`Name`}),(0,l.jsx)(s,{id:`name`,className:`mt-1 block w-full`,value:p.name,onChange:e=>m(`name`,e.target.value),required:!0,isFocused:!0,autoComplete:`name`}),(0,l.jsx)(i,{className:`mt-2`,message:g.name})]}),(0,l.jsxs)(`div`,{children:[(0,l.jsx)(a,{htmlFor:`email`,value:`Email`}),(0,l.jsx)(s,{id:`email`,type:`email`,className:`mt-1 block w-full`,value:p.email,onChange:e=>m(`email`,e.target.value),required:!0,autoComplete:`username`}),(0,l.jsx)(i,{className:`mt-2`,message:g.email})]}),r&&f.email_verified_at===null&&(0,l.jsxs)(`div`,{children:[(0,l.jsxs)(`p`,{className:`mt-2 text-sm text-gray-800`,children:[`Your email address is unverified.`,(0,l.jsx)(n,{href:route(`verification.send`),method:`post`,as:`button`,className:`rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2`,children:`Click here to re-send the verification email.`})]}),u===`verification-link-sent`&&(0,l.jsx)(`div`,{className:`mt-2 text-sm font-medium text-green-600`,children:`A new verification link has been sent to your email address.`})]}),(0,l.jsxs)(`div`,{className:`flex items-center gap-4`,children:[(0,l.jsx)(c,{disabled:_,children:`Save`}),(0,l.jsx)(o,{show:v,enter:`transition ease-in-out`,enterFrom:`opacity-0`,leave:`transition ease-in-out`,leaveTo:`opacity-0`,children:(0,l.jsx)(`p`,{className:`text-sm text-gray-600`,children:`Saved.`})})]})]})]})}export{u as default}; import{a as e,o as t,r as n,t as r}from"./app-CR33GSSe.js";import{C as i,S as a,n as o,t as s}from"./TextInput-Ci82coVx.js";import{t as c}from"./PrimaryButton-DSkvm77Z.js";var l=r();function u({mustVerifyEmail:r,status:u,className:d=``}){let f=t().props.auth.user,{data:p,setData:m,patch:h,errors:g,processing:_,recentlySuccessful:v}=e({name:f.name,email:f.email});return(0,l.jsxs)(`section`,{className:d,children:[(0,l.jsxs)(`header`,{children:[(0,l.jsx)(`h2`,{className:`text-lg font-medium text-gray-900`,children:`Profile Information`}),(0,l.jsx)(`p`,{className:`mt-1 text-sm text-gray-600`,children:`Update your account's profile information and email address.`})]}),(0,l.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),h(route(`profile.update`))},className:`mt-6 space-y-6`,children:[(0,l.jsxs)(`div`,{children:[(0,l.jsx)(a,{htmlFor:`name`,value:`Name`}),(0,l.jsx)(s,{id:`name`,className:`mt-1 block w-full`,value:p.name,onChange:e=>m(`name`,e.target.value),required:!0,isFocused:!0,autoComplete:`name`}),(0,l.jsx)(i,{className:`mt-2`,message:g.name})]}),(0,l.jsxs)(`div`,{children:[(0,l.jsx)(a,{htmlFor:`email`,value:`Email`}),(0,l.jsx)(s,{id:`email`,type:`email`,className:`mt-1 block w-full`,value:p.email,onChange:e=>m(`email`,e.target.value),required:!0,autoComplete:`username`}),(0,l.jsx)(i,{className:`mt-2`,message:g.email})]}),r&&f.email_verified_at===null&&(0,l.jsxs)(`div`,{children:[(0,l.jsxs)(`p`,{className:`mt-2 text-sm text-gray-800`,children:[`Your email address is unverified.`,(0,l.jsx)(n,{href:route(`verification.send`),method:`post`,as:`button`,className:`rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2`,children:`Click here to re-send the verification email.`})]}),u===`verification-link-sent`&&(0,l.jsx)(`div`,{className:`mt-2 text-sm font-medium text-green-600`,children:`A new verification link has been sent to your email address.`})]}),(0,l.jsxs)(`div`,{className:`flex items-center gap-4`,children:[(0,l.jsx)(c,{disabled:_,children:`Save`}),(0,l.jsx)(o,{show:v,enter:`transition ease-in-out`,enterFrom:`opacity-0`,leave:`transition ease-in-out`,leaveTo:`opacity-0`,children:(0,l.jsx)(`p`,{className:`text-sm text-gray-600`,children:`Saved.`})})]})]})]})}export{u as default};
@@ -1 +1 @@
import{a as e,c as t,n,r,t as i}from"./app-BJ7g6sa8.js";import{t as a}from"./GuestLayout-CN-YY0cs.js";t();var o=i();function s({status:t}){let{post:i,processing:s}=e({});return(0,o.jsxs)(a,{children:[(0,o.jsx)(n,{title:`Verify email`}),(0,o.jsxs)(`div`,{className:`mb-8 anim-down`,children:[(0,o.jsx)(`div`,{className:`w-12 h-12 bg-[#3D4E4B]/5 rounded-2xl flex items-center justify-center mb-6`,children:(0,o.jsx)(`svg`,{className:`w-5 h-5 text-[#3D4E4B]`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2,children:(0,o.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z`})})}),(0,o.jsx)(`h1`,{className:`text-2xl font-bold text-[#1A2421] tracking-tight`,children:`Check your email`}),(0,o.jsx)(`p`,{className:`mt-1.5 text-sm text-gray-400 font-medium leading-relaxed`,children:`We sent a verification link to your email address. Click the link to activate your account.`})]}),t===`verification-link-sent`&&(0,o.jsx)(`div`,{className:`mb-6 px-4 py-3 rounded-xl bg-emerald-50 border border-emerald-100 text-sm font-semibold text-emerald-700 anim-fade`,children:`A new verification link has been sent to your email.`}),(0,o.jsx)(`form`,{onSubmit:e=>{e.preventDefault(),i(route(`verification.send`))},className:`anim-up`,style:{animationDelay:`0.1s`},children:(0,o.jsx)(`button`,{type:`submit`,disabled:s,className:`w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed`,children:s?(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(`svg`,{className:`w-4 h-4 animate-spin text-white/60`,fill:`none`,viewBox:`0 0 24 24`,children:[(0,o.jsx)(`circle`,{className:`opacity-25`,cx:`12`,cy:`12`,r:`10`,stroke:`currentColor`,strokeWidth:`4`}),(0,o.jsx)(`path`,{className:`opacity-75`,fill:`currentColor`,d:`M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z`})]}),`Sending…`]}):`Resend verification email`})}),(0,o.jsx)(`div`,{className:`mt-5 text-center anim-fade`,style:{animationDelay:`0.18s`},children:(0,o.jsx)(r,{href:route(`logout`),method:`post`,as:`button`,className:`text-sm font-semibold text-gray-400 hover:text-[#3D4E4B] transition-colors duration-200`,children:`Sign out`})})]})}export{s as default}; import{a as e,c as t,n,r,t as i}from"./app-CR33GSSe.js";import{t as a}from"./GuestLayout-RqG17kT8.js";t();var o=i();function s({status:t}){let{post:i,processing:s}=e({});return(0,o.jsxs)(a,{children:[(0,o.jsx)(n,{title:`Verify email`}),(0,o.jsxs)(`div`,{className:`mb-8 anim-down`,children:[(0,o.jsx)(`div`,{className:`w-12 h-12 bg-[#3D4E4B]/5 rounded-2xl flex items-center justify-center mb-6`,children:(0,o.jsx)(`svg`,{className:`w-5 h-5 text-[#3D4E4B]`,fill:`none`,viewBox:`0 0 24 24`,stroke:`currentColor`,strokeWidth:2,children:(0,o.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z`})})}),(0,o.jsx)(`h1`,{className:`text-2xl font-bold text-[#1A2421] tracking-tight`,children:`Check your email`}),(0,o.jsx)(`p`,{className:`mt-1.5 text-sm text-gray-400 font-medium leading-relaxed`,children:`We sent a verification link to your email address. Click the link to activate your account.`})]}),t===`verification-link-sent`&&(0,o.jsx)(`div`,{className:`mb-6 px-4 py-3 rounded-xl bg-emerald-50 border border-emerald-100 text-sm font-semibold text-emerald-700 anim-fade`,children:`A new verification link has been sent to your email.`}),(0,o.jsx)(`form`,{onSubmit:e=>{e.preventDefault(),i(route(`verification.send`))},className:`anim-up`,style:{animationDelay:`0.1s`},children:(0,o.jsx)(`button`,{type:`submit`,disabled:s,className:`w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed`,children:s?(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(`svg`,{className:`w-4 h-4 animate-spin text-white/60`,fill:`none`,viewBox:`0 0 24 24`,children:[(0,o.jsx)(`circle`,{className:`opacity-25`,cx:`12`,cy:`12`,r:`10`,stroke:`currentColor`,strokeWidth:`4`}),(0,o.jsx)(`path`,{className:`opacity-75`,fill:`currentColor`,d:`M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z`})]}),`Sending…`]}):`Resend verification email`})}),(0,o.jsx)(`div`,{className:`mt-5 text-center anim-fade`,style:{animationDelay:`0.18s`},children:(0,o.jsx)(r,{href:route(`logout`),method:`post`,as:`button`,className:`text-sm font-semibold text-gray-400 hover:text-[#3D4E4B] transition-colors duration-200`,children:`Sign out`})})]})}export{s as default};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{c as e,n as t,t as n}from"./app-BJ7g6sa8.js";e();var r=n();function i({auth:e}){return(0,r.jsxs)(`div`,{className:`min-h-screen bg-[#E3EBE8] text-[#3D4E4B] font-sans`,children:[(0,r.jsx)(t,{title:`Xxx`}),(0,r.jsxs)(`div`,{className:`max-w-7xl mx-auto px-6 py-12`,children:[(0,r.jsx)(`h1`,{className:`text-3xl font-bold mb-8`,children:`Xxx Page`}),(0,r.jsx)(`div`,{className:`bg-white rounded-lg shadow-sm p-6`,children:(0,r.jsx)(`p`,{children:`This is the new xxx page content.`})})]})]})}export{i as default}; import{c as e,n as t,t as n}from"./app-CR33GSSe.js";e();var r=n();function i({auth:e}){return(0,r.jsxs)(`div`,{className:`min-h-screen bg-[#E3EBE8] text-[#3D4E4B] font-sans`,children:[(0,r.jsx)(t,{title:`Xxx`}),(0,r.jsxs)(`div`,{className:`max-w-7xl mx-auto px-6 py-12`,children:[(0,r.jsx)(`h1`,{className:`text-3xl font-bold mb-8`,children:`Xxx Page`}),(0,r.jsx)(`div`,{className:`bg-white rounded-lg shadow-sm p-6`,children:(0,r.jsx)(`p`,{children:`This is the new xxx page content.`})})]})]})}export{i as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{d as e,u as t}from"./app-BJ7g6sa8.js";var n=t(((e,t)=>{(function(n,r){typeof e==`object`&&t!==void 0?t.exports=r():typeof define==`function`&&define.amd?define(r):(n=typeof globalThis<`u`?globalThis:n||self,n.Sweetalert2=r())})(e,(function(){function e(e,t,n){if(typeof e==`function`?e===t:e.has(t))return arguments.length<3?t:n;throw TypeError(`Private element is not present on this object`)}function t(e,t){if(t.has(e))throw TypeError(`Cannot initialize the same private elements twice on an object`)}function n(t,n){return t.get(e(t,n))}function r(e,n,r){t(e,n),n.set(e,r)}function i(t,n,r){return t.set(e(t,n),r),r}let a={},o=()=>{a.previousActiveElement instanceof HTMLElement?(a.previousActiveElement.focus(),a.previousActiveElement=null):document.body&&document.body.focus()},s=e=>new Promise(t=>{if(!e)return t();let n=window.scrollX,r=window.scrollY;a.restoreFocusTimeout=setTimeout(()=>{o(),t()},100),window.scrollTo(n,r)}),ee=`swal2-`,c=`container.shown.height-auto.iosfix.popup.modal.no-backdrop.no-transition.toast.toast-shown.show.hide.close.title.html-container.actions.confirm.deny.cancel.footer.icon.icon-content.image.input.file.range.select.radio.checkbox.label.textarea.inputerror.input-label.validation-message.progress-steps.active-progress-step.progress-step.progress-step-line.loader.loading.styled.top.top-start.top-end.top-left.top-right.center.center-start.center-end.center-left.center-right.bottom.bottom-start.bottom-end.bottom-left.bottom-right.grow-row.grow-column.grow-fullscreen.rtl.timer-progress-bar.timer-progress-bar-container.scrollbar-measure.icon-success.icon-warning.icon-info.icon-question.icon-error.draggable.dragging`.split(`.`).reduce((e,t)=>(e[t]=ee+t,e),{}),l=[`success`,`warning`,`info`,`question`,`error`].reduce((e,t)=>(e[t]=ee+t,e),{}),te=`SweetAlert2:`,ne=e=>e.charAt(0).toUpperCase()+e.slice(1),u=e=>{console.warn(`${te} ${typeof e==`object`?e.join(` `):e}`)},d=e=>{console.error(`${te} ${e}`)},re=[],ie=e=>{re.includes(e)||(re.push(e),u(e))},ae=(e,t=null)=>{ie(`"${e}" is deprecated and will be removed in the next major release.${t?` Use "${t}" instead.`:``}`)},oe=e=>typeof e==`function`?e():e,se=e=>e&&typeof e.toPromise==`function`,f=e=>se(e)?e.toPromise():Promise.resolve(e),ce=e=>e&&Promise.resolve(e)===e,le=()=>navigator.userAgent.includes(`Firefox`),p=()=>document.body.querySelector(`.${c.container}`),m=e=>{let t=p();return t?t.querySelector(e):null},h=e=>m(`.${e}`),g=()=>h(c.popup),_=()=>h(c.icon),ue=()=>h(c[`icon-content`]),de=()=>h(c.title),fe=()=>h(c[`html-container`]),pe=()=>h(c.image),me=()=>h(c[`progress-steps`]),he=()=>h(c[`validation-message`]),v=()=>m(`.${c.actions} .${c.confirm}`),y=()=>m(`.${c.actions} .${c.cancel}`),b=()=>m(`.${c.actions} .${c.deny}`),ge=()=>h(c[`input-label`]),x=()=>m(`.${c.loader}`),S=()=>h(c.actions),_e=()=>h(c.footer),ve=()=>h(c[`timer-progress-bar`]),ye=()=>h(c.close),be=()=>{let e=g();if(!e)return[];let t=e.querySelectorAll(`[tabindex]:not([tabindex="-1"]):not([tabindex="0"])`),n=Array.from(t).sort((e,t)=>{let n=parseInt(e.getAttribute(`tabindex`)||`0`),r=parseInt(t.getAttribute(`tabindex`)||`0`);return n>r?1:n<r?-1:0}),r=e.querySelectorAll(` import{d as e,u as t}from"./app-CR33GSSe.js";var n=t(((e,t)=>{(function(n,r){typeof e==`object`&&t!==void 0?t.exports=r():typeof define==`function`&&define.amd?define(r):(n=typeof globalThis<`u`?globalThis:n||self,n.Sweetalert2=r())})(e,(function(){function e(e,t,n){if(typeof e==`function`?e===t:e.has(t))return arguments.length<3?t:n;throw TypeError(`Private element is not present on this object`)}function t(e,t){if(t.has(e))throw TypeError(`Cannot initialize the same private elements twice on an object`)}function n(t,n){return t.get(e(t,n))}function r(e,n,r){t(e,n),n.set(e,r)}function i(t,n,r){return t.set(e(t,n),r),r}let a={},o=()=>{a.previousActiveElement instanceof HTMLElement?(a.previousActiveElement.focus(),a.previousActiveElement=null):document.body&&document.body.focus()},s=e=>new Promise(t=>{if(!e)return t();let n=window.scrollX,r=window.scrollY;a.restoreFocusTimeout=setTimeout(()=>{o(),t()},100),window.scrollTo(n,r)}),ee=`swal2-`,c=`container.shown.height-auto.iosfix.popup.modal.no-backdrop.no-transition.toast.toast-shown.show.hide.close.title.html-container.actions.confirm.deny.cancel.footer.icon.icon-content.image.input.file.range.select.radio.checkbox.label.textarea.inputerror.input-label.validation-message.progress-steps.active-progress-step.progress-step.progress-step-line.loader.loading.styled.top.top-start.top-end.top-left.top-right.center.center-start.center-end.center-left.center-right.bottom.bottom-start.bottom-end.bottom-left.bottom-right.grow-row.grow-column.grow-fullscreen.rtl.timer-progress-bar.timer-progress-bar-container.scrollbar-measure.icon-success.icon-warning.icon-info.icon-question.icon-error.draggable.dragging`.split(`.`).reduce((e,t)=>(e[t]=ee+t,e),{}),l=[`success`,`warning`,`info`,`question`,`error`].reduce((e,t)=>(e[t]=ee+t,e),{}),te=`SweetAlert2:`,ne=e=>e.charAt(0).toUpperCase()+e.slice(1),u=e=>{console.warn(`${te} ${typeof e==`object`?e.join(` `):e}`)},d=e=>{console.error(`${te} ${e}`)},re=[],ie=e=>{re.includes(e)||(re.push(e),u(e))},ae=(e,t=null)=>{ie(`"${e}" is deprecated and will be removed in the next major release.${t?` Use "${t}" instead.`:``}`)},oe=e=>typeof e==`function`?e():e,se=e=>e&&typeof e.toPromise==`function`,f=e=>se(e)?e.toPromise():Promise.resolve(e),ce=e=>e&&Promise.resolve(e)===e,le=()=>navigator.userAgent.includes(`Firefox`),p=()=>document.body.querySelector(`.${c.container}`),m=e=>{let t=p();return t?t.querySelector(e):null},h=e=>m(`.${e}`),g=()=>h(c.popup),_=()=>h(c.icon),ue=()=>h(c[`icon-content`]),de=()=>h(c.title),fe=()=>h(c[`html-container`]),pe=()=>h(c.image),me=()=>h(c[`progress-steps`]),he=()=>h(c[`validation-message`]),v=()=>m(`.${c.actions} .${c.confirm}`),y=()=>m(`.${c.actions} .${c.cancel}`),b=()=>m(`.${c.actions} .${c.deny}`),ge=()=>h(c[`input-label`]),x=()=>m(`.${c.loader}`),S=()=>h(c.actions),_e=()=>h(c.footer),ve=()=>h(c[`timer-progress-bar`]),ye=()=>h(c.close),be=()=>{let e=g();if(!e)return[];let t=e.querySelectorAll(`[tabindex]:not([tabindex="-1"]):not([tabindex="0"])`),n=Array.from(t).sort((e,t)=>{let n=parseInt(e.getAttribute(`tabindex`)||`0`),r=parseInt(t.getAttribute(`tabindex`)||`0`);return n>r?1:n<r?-1:0}),r=e.querySelectorAll(`
a[href], a[href],
area[href], area[href],
input:not([disabled]), input:not([disabled]),
+89 -88
View File
@@ -1,48 +1,52 @@
{ {
"_AuthenticatedLayout-CrB9BCoI.js": { "_AuthenticatedLayout-BH8u_p2M.js": {
"file": "assets/AuthenticatedLayout-CrB9BCoI.js", "file": "assets/AuthenticatedLayout-BH8u_p2M.js",
"name": "AuthenticatedLayout", "name": "AuthenticatedLayout",
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx"
] ]
}, },
"_Can-DIOq7dyw.js": { "_Can-C48_nAl_.js": {
"file": "assets/Can-DIOq7dyw.js", "file": "assets/Can-C48_nAl_.js",
"name": "Can", "name": "Can",
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx"
] ]
}, },
"_GuestLayout-CN-YY0cs.js": { "_GuestLayout-RqG17kT8.js": {
"file": "assets/GuestLayout-CN-YY0cs.js", "file": "assets/GuestLayout-RqG17kT8.js",
"name": "GuestLayout", "name": "GuestLayout",
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx"
] ]
}, },
"_Portal-DJbp1s68.js": { "_Portal-yh3eCFHT.js": {
"file": "assets/Portal-DJbp1s68.js", "file": "assets/Portal-yh3eCFHT.js",
"name": "Portal", "name": "Portal",
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx"
] ]
}, },
"_PrimaryButton-KeVcwQeg.js": { "_PrimaryButton-DSkvm77Z.js": {
"file": "assets/PrimaryButton-KeVcwQeg.js", "file": "assets/PrimaryButton-DSkvm77Z.js",
"name": "PrimaryButton", "name": "PrimaryButton",
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx"
] ]
}, },
"_TextInput-DV7QeRn3.js": { "_TextInput-Ci82coVx.js": {
"file": "assets/TextInput-DV7QeRn3.js", "file": "assets/TextInput-Ci82coVx.js",
"name": "TextInput", "name": "TextInput",
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx"
] ]
}, },
"_filepond-plugin-file-validate-type-CBUe71W_.js": { "_filepond-plugin-file-validate-type-CEtEkCs1.css": {
"file": "assets/filepond-plugin-file-validate-type-CBUe71W_.js", "file": "assets/filepond-plugin-file-validate-type-CEtEkCs1.css",
"src": "_filepond-plugin-file-validate-type-CEtEkCs1.css"
},
"_filepond-plugin-file-validate-type-D7_-e7aX.js": {
"file": "assets/filepond-plugin-file-validate-type-D7_-e7aX.js",
"name": "filepond-plugin-file-validate-type", "name": "filepond-plugin-file-validate-type",
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx"
@@ -51,19 +55,15 @@
"assets/filepond-plugin-file-validate-type-CEtEkCs1.css" "assets/filepond-plugin-file-validate-type-CEtEkCs1.css"
] ]
}, },
"_filepond-plugin-file-validate-type-CEtEkCs1.css": { "_lodash-BbZKcgcd.js": {
"file": "assets/filepond-plugin-file-validate-type-CEtEkCs1.css", "file": "assets/lodash-BbZKcgcd.js",
"src": "_filepond-plugin-file-validate-type-CEtEkCs1.css"
},
"_lodash-ZrZcSXd_.js": {
"file": "assets/lodash-ZrZcSXd_.js",
"name": "lodash", "name": "lodash",
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx"
] ]
}, },
"_swal-DZXjpqDE.js": { "_swal-DIkHduCM.js": {
"file": "assets/swal-DZXjpqDE.js", "file": "assets/swal-DIkHduCM.js",
"name": "swal", "name": "swal",
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx"
@@ -77,99 +77,99 @@
"src": "_swal-DtpL8WXZ.css" "src": "_swal-DtpL8WXZ.css"
}, },
"resources/js/Pages/ActivityLogs/Index.tsx": { "resources/js/Pages/ActivityLogs/Index.tsx": {
"file": "assets/Index-Cm49_1vz.js", "file": "assets/Index-COrUYi4Q.js",
"name": "Index", "name": "Index",
"src": "resources/js/Pages/ActivityLogs/Index.tsx", "src": "resources/js/Pages/ActivityLogs/Index.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_lodash-ZrZcSXd_.js", "_lodash-BbZKcgcd.js",
"_Portal-DJbp1s68.js", "_Portal-yh3eCFHT.js",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/Auth/ConfirmPassword.tsx": { "resources/js/Pages/Auth/ConfirmPassword.tsx": {
"file": "assets/ConfirmPassword-Ducw29r7.js", "file": "assets/ConfirmPassword-BJSwlYyR.js",
"name": "ConfirmPassword", "name": "ConfirmPassword",
"src": "resources/js/Pages/Auth/ConfirmPassword.tsx", "src": "resources/js/Pages/Auth/ConfirmPassword.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_GuestLayout-CN-YY0cs.js" "_GuestLayout-RqG17kT8.js"
] ]
}, },
"resources/js/Pages/Auth/ForgotPassword.tsx": { "resources/js/Pages/Auth/ForgotPassword.tsx": {
"file": "assets/ForgotPassword-BmQrO4Bp.js", "file": "assets/ForgotPassword-BMmmpf3n.js",
"name": "ForgotPassword", "name": "ForgotPassword",
"src": "resources/js/Pages/Auth/ForgotPassword.tsx", "src": "resources/js/Pages/Auth/ForgotPassword.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_GuestLayout-CN-YY0cs.js" "_GuestLayout-RqG17kT8.js"
] ]
}, },
"resources/js/Pages/Auth/Login.tsx": { "resources/js/Pages/Auth/Login.tsx": {
"file": "assets/Login-DUDEFmAx.js", "file": "assets/Login-BWk-Nbxy.js",
"name": "Login", "name": "Login",
"src": "resources/js/Pages/Auth/Login.tsx", "src": "resources/js/Pages/Auth/Login.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_GuestLayout-CN-YY0cs.js" "_GuestLayout-RqG17kT8.js"
] ]
}, },
"resources/js/Pages/Auth/Register.tsx": { "resources/js/Pages/Auth/Register.tsx": {
"file": "assets/Register-BscYc22x.js", "file": "assets/Register-Bmtx71Oh.js",
"name": "Register", "name": "Register",
"src": "resources/js/Pages/Auth/Register.tsx", "src": "resources/js/Pages/Auth/Register.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_GuestLayout-CN-YY0cs.js" "_GuestLayout-RqG17kT8.js"
] ]
}, },
"resources/js/Pages/Auth/ResetPassword.tsx": { "resources/js/Pages/Auth/ResetPassword.tsx": {
"file": "assets/ResetPassword-BTTfjVJh.js", "file": "assets/ResetPassword-eh7b21to.js",
"name": "ResetPassword", "name": "ResetPassword",
"src": "resources/js/Pages/Auth/ResetPassword.tsx", "src": "resources/js/Pages/Auth/ResetPassword.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_GuestLayout-CN-YY0cs.js" "_GuestLayout-RqG17kT8.js"
] ]
}, },
"resources/js/Pages/Auth/VerifyEmail.tsx": { "resources/js/Pages/Auth/VerifyEmail.tsx": {
"file": "assets/VerifyEmail-rqxgUqlx.js", "file": "assets/VerifyEmail-BBtJsxiK.js",
"name": "VerifyEmail", "name": "VerifyEmail",
"src": "resources/js/Pages/Auth/VerifyEmail.tsx", "src": "resources/js/Pages/Auth/VerifyEmail.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_GuestLayout-CN-YY0cs.js" "_GuestLayout-RqG17kT8.js"
] ]
}, },
"resources/js/Pages/Dashboard.tsx": { "resources/js/Pages/Dashboard.tsx": {
"file": "assets/Dashboard-DZOwY3RZ.js", "file": "assets/Dashboard-D4Q6wEON.js",
"name": "Dashboard", "name": "Dashboard",
"src": "resources/js/Pages/Dashboard.tsx", "src": "resources/js/Pages/Dashboard.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/Docs/Index.tsx": { "resources/js/Pages/Docs/Index.tsx": {
"file": "assets/Index-CgghpLIe.js", "file": "assets/Index-PMGZGGiO.js",
"name": "Index", "name": "Index",
"src": "resources/js/Pages/Docs/Index.tsx", "src": "resources/js/Pages/Docs/Index.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/Errors/Error.tsx": { "resources/js/Pages/Errors/Error.tsx": {
"file": "assets/Error-rwcY_Rc-.js", "file": "assets/Error-CNvOUas0.js",
"name": "Error", "name": "Error",
"src": "resources/js/Pages/Errors/Error.tsx", "src": "resources/js/Pages/Errors/Error.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -178,143 +178,144 @@
] ]
}, },
"resources/js/Pages/Notifications/Index.tsx": { "resources/js/Pages/Notifications/Index.tsx": {
"file": "assets/Index-DLpY0zj1.js", "file": "assets/Index-CqyofaaN.js",
"name": "Index", "name": "Index",
"src": "resources/js/Pages/Notifications/Index.tsx", "src": "resources/js/Pages/Notifications/Index.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_swal-DZXjpqDE.js", "_swal-DIkHduCM.js",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/Profile/Edit.tsx": { "resources/js/Pages/Profile/Edit.tsx": {
"file": "assets/Edit-BCh5fCTK.js", "file": "assets/Edit-BC1lrTIM.js",
"name": "Edit", "name": "Edit",
"src": "resources/js/Pages/Profile/Edit.tsx", "src": "resources/js/Pages/Profile/Edit.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_filepond-plugin-file-validate-type-CBUe71W_.js", "_filepond-plugin-file-validate-type-D7_-e7aX.js",
"_swal-DZXjpqDE.js", "_swal-DIkHduCM.js",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/Profile/Partials/DeleteUserForm.tsx": { "resources/js/Pages/Profile/Partials/DeleteUserForm.tsx": {
"file": "assets/DeleteUserForm-DLkG3tvo.js", "file": "assets/DeleteUserForm-Df0bgF3F.js",
"name": "DeleteUserForm", "name": "DeleteUserForm",
"src": "resources/js/Pages/Profile/Partials/DeleteUserForm.tsx", "src": "resources/js/Pages/Profile/Partials/DeleteUserForm.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_TextInput-DV7QeRn3.js" "_TextInput-Ci82coVx.js"
] ]
}, },
"resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx": { "resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx": {
"file": "assets/UpdatePasswordForm-DXZy5eGn.js", "file": "assets/UpdatePasswordForm-BDonTGx4.js",
"name": "UpdatePasswordForm", "name": "UpdatePasswordForm",
"src": "resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx", "src": "resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_TextInput-DV7QeRn3.js", "_TextInput-Ci82coVx.js",
"_PrimaryButton-KeVcwQeg.js" "_PrimaryButton-DSkvm77Z.js"
] ]
}, },
"resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.tsx": { "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.tsx": {
"file": "assets/UpdateProfileInformationForm-CZPCM5rZ.js", "file": "assets/UpdateProfileInformationForm-cbpXxqCM.js",
"name": "UpdateProfileInformationForm", "name": "UpdateProfileInformationForm",
"src": "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.tsx", "src": "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_TextInput-DV7QeRn3.js", "_TextInput-Ci82coVx.js",
"_PrimaryButton-KeVcwQeg.js" "_PrimaryButton-DSkvm77Z.js"
] ]
}, },
"resources/js/Pages/Roles/Index.tsx": { "resources/js/Pages/Roles/Index.tsx": {
"file": "assets/Index-Bnf5l0xj.js", "file": "assets/Index-DsJ3tpK-.js",
"name": "Index", "name": "Index",
"src": "resources/js/Pages/Roles/Index.tsx", "src": "resources/js/Pages/Roles/Index.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_swal-DZXjpqDE.js", "_swal-DIkHduCM.js",
"_Can-DIOq7dyw.js", "_Can-C48_nAl_.js",
"_Portal-DJbp1s68.js", "_Portal-yh3eCFHT.js",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/Settings/Index.tsx": { "resources/js/Pages/Settings/Index.tsx": {
"file": "assets/Index-z4H1ItiM.js", "file": "assets/Index-CQNozTj5.js",
"name": "Index", "name": "Index",
"src": "resources/js/Pages/Settings/Index.tsx", "src": "resources/js/Pages/Settings/Index.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_filepond-plugin-file-validate-type-CBUe71W_.js", "_filepond-plugin-file-validate-type-D7_-e7aX.js",
"_swal-DZXjpqDE.js", "_swal-DIkHduCM.js",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/SystemSettings/Index.tsx": { "resources/js/Pages/SystemSettings/Index.tsx": {
"file": "assets/Index-A9YntmU6.js", "file": "assets/Index-Cd5Q7vjy.js",
"name": "Index", "name": "Index",
"src": "resources/js/Pages/SystemSettings/Index.tsx", "src": "resources/js/Pages/SystemSettings/Index.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_filepond-plugin-file-validate-type-CBUe71W_.js", "_filepond-plugin-file-validate-type-D7_-e7aX.js",
"_swal-DZXjpqDE.js", "_swal-DIkHduCM.js",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/TwoFactor/Challenge.tsx": { "resources/js/Pages/TwoFactor/Challenge.tsx": {
"file": "assets/Challenge-DaBnX94x.js", "file": "assets/Challenge-Dt5F_K19.js",
"name": "Challenge", "name": "Challenge",
"src": "resources/js/Pages/TwoFactor/Challenge.tsx", "src": "resources/js/Pages/TwoFactor/Challenge.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx" "resources/js/app.tsx",
"_swal-DIkHduCM.js"
] ]
}, },
"resources/js/Pages/TwoFactor/Setup.tsx": { "resources/js/Pages/TwoFactor/Setup.tsx": {
"file": "assets/Setup-ZspLG92f.js", "file": "assets/Setup-CDKYaVgf.js",
"name": "Setup", "name": "Setup",
"src": "resources/js/Pages/TwoFactor/Setup.tsx", "src": "resources/js/Pages/TwoFactor/Setup.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_swal-DZXjpqDE.js", "_swal-DIkHduCM.js",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/Users/Index.tsx": { "resources/js/Pages/Users/Index.tsx": {
"file": "assets/Index-cmCbZg8n.js", "file": "assets/Index-B5-PvHPS.js",
"name": "Index", "name": "Index",
"src": "resources/js/Pages/Users/Index.tsx", "src": "resources/js/Pages/Users/Index.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_lodash-ZrZcSXd_.js", "_lodash-BbZKcgcd.js",
"_swal-DZXjpqDE.js", "_swal-DIkHduCM.js",
"_Can-DIOq7dyw.js", "_Can-C48_nAl_.js",
"_Portal-DJbp1s68.js", "_Portal-yh3eCFHT.js",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/Users/Show.tsx": { "resources/js/Pages/Users/Show.tsx": {
"file": "assets/Show-lMRyP8VR.js", "file": "assets/Show-Bv8xOU0W.js",
"name": "Show", "name": "Show",
"src": "resources/js/Pages/Users/Show.tsx", "src": "resources/js/Pages/Users/Show.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"resources/js/app.tsx", "resources/js/app.tsx",
"_AuthenticatedLayout-CrB9BCoI.js" "_AuthenticatedLayout-BH8u_p2M.js"
] ]
}, },
"resources/js/Pages/Welcome.tsx": { "resources/js/Pages/Welcome.tsx": {
"file": "assets/Welcome-CDP6Hme4.js", "file": "assets/Welcome-VQFuCFbP.js",
"name": "Welcome", "name": "Welcome",
"src": "resources/js/Pages/Welcome.tsx", "src": "resources/js/Pages/Welcome.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -323,7 +324,7 @@
] ]
}, },
"resources/js/Pages/Xxx.tsx": { "resources/js/Pages/Xxx.tsx": {
"file": "assets/Xxx-CagXuP8t.js", "file": "assets/Xxx-DguSj5Ba.js",
"name": "Xxx", "name": "Xxx",
"src": "resources/js/Pages/Xxx.tsx", "src": "resources/js/Pages/Xxx.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -332,7 +333,7 @@
] ]
}, },
"resources/js/app.tsx": { "resources/js/app.tsx": {
"file": "assets/app-BJ7g6sa8.js", "file": "assets/app-CR33GSSe.js",
"name": "app", "name": "app",
"src": "resources/js/app.tsx", "src": "resources/js/app.tsx",
"isEntry": true, "isEntry": true,
@@ -363,7 +364,7 @@
"resources/js/Pages/Xxx.tsx" "resources/js/Pages/Xxx.tsx"
], ],
"css": [ "css": [
"assets/app-Bu4ihmu3.css" "assets/app-BEFRBVv6.css"
] ]
} }
} }
+1
View File
@@ -0,0 +1 @@
http://localhost:5173
+5 -5
View File
@@ -27,7 +27,7 @@ export function Sidebar({ theme: _theme }: { theme: string }) {
{ {
href: '/notifications', href: '/notifications',
label: 'Notifications', label: 'Notifications',
ability: 'role.manage', ability: 'notifications.view',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /></svg> icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /></svg>
}, },
] ]
@@ -50,7 +50,7 @@ export function Sidebar({ theme: _theme }: { theme: string }) {
{ {
href: '/activity-logs', href: '/activity-logs',
label: 'Activity Logs', label: 'Activity Logs',
ability: 'user.view', ability: 'activity-logs.view',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
}, },
] ]
@@ -73,20 +73,20 @@ export function Sidebar({ theme: _theme }: { theme: string }) {
{ {
href: '/system-settings', href: '/system-settings',
label: 'System Settings', label: 'System Settings',
ability: null, ability: 'settings.view',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" 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 strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg> icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" 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 strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
}, },
{ {
href: '/documentation', href: '/documentation',
label: 'Dokumentasi', label: 'Dokumentasi',
ability: null, ability: 'documentation.view',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg> icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>
}, },
] ]
}, },
]; ];
const canManageSettings = permissions.includes('settings.manage'); const canManageSettings = permissions.includes('settings.view');
return ( return (
<aside className="w-[260px] h-screen p-4 shrink-0 relative z-20 flex flex-col anim-left"> <aside className="w-[260px] h-screen p-4 shrink-0 relative z-20 flex flex-col anim-left">
+48 -17
View File
@@ -312,10 +312,22 @@ Content-Type: application/json
{ perm: 'user.create', u: false, a: true, s: true }, { perm: 'user.create', u: false, a: true, s: true },
{ perm: 'user.edit', u: false, a: true, s: true }, { perm: 'user.edit', u: false, a: true, s: true },
{ perm: 'user.delete', u: false, a: true, s: true }, { perm: 'user.delete', u: false, a: true, s: true },
{ perm: 'user.restore', u: false, a: true, s: true },
{ perm: 'user.force-delete', u: false, a: false, s: true },
{ perm: 'user.export', u: false, a: true, s: true },
{ perm: 'user.import', u: false, a: true, s: true },
{ perm: 'role.view', u: false, a: true, s: true }, { perm: 'role.view', u: false, a: true, s: true },
{ perm: 'role.manage', u: false, a: false, s: true }, { perm: 'role.create', u: false, a: true, s: true },
{ perm: 'settings.manage',u: false, a: false, s: true }, { perm: 'role.delete', u: false, a: true, s: true },
{ perm: 'reports.view', u: false, a: true, s: true }, { perm: 'role.manage', u: false, a: true, s: true },
{ perm: 'notifications.view', u: false, a: true, s: true },
{ perm: 'notifications.send', u: false, a: true, s: true },
{ perm: 'activity-logs.view', u: false, a: true, s: true },
{ perm: 'activity-logs.delete', u: false, a: false, s: true },
{ perm: 'settings.view', u: false, a: false, s: true },
{ perm: 'settings.edit', u: false, a: false, s: true },
{ perm: 'settings.test-email', u: false, a: false, s: true },
{ perm: 'documentation.view', u: false, a: false, s: true },
].map(row => ( ].map(row => (
<tr key={row.perm} className="border-b border-gray-50 dark:border-white/5 last:border-0"> <tr key={row.perm} className="border-b border-gray-50 dark:border-white/5 last:border-0">
<td className="py-3 pr-6 font-mono font-bold text-[#3D4E4B] dark:text-white">{row.perm}</td> <td className="py-3 pr-6 font-mono font-bold text-[#3D4E4B] dark:text-white">{row.perm}</td>
@@ -493,18 +505,39 @@ $this->authorize('user.delete');
{/* ── 2FA ── */} {/* ── 2FA ── */}
<section> <section>
<SectionHeader id="2fa" title="Two-Factor Authentication" badge="TOTP" badgeColor="amber" /> <SectionHeader id="2fa" title="Two-Factor Authentication" badge="TOTP + EMAIL" badgeColor="amber" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-4"> <div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-6">
<p className="text-sm text-gray-500 dark:text-gray-400 font-medium leading-relaxed"> <p className="text-sm text-gray-500 dark:text-gray-400 font-medium leading-relaxed">
2FA menggunakan protokol <strong className="text-[#3D4E4B] dark:text-white">TOTP (Time-based One-Time Password)</strong> yang kompatibel dengan Google Authenticator, Authy, dan 1Password. Sistem mendukung dual-modality 2FA: <strong className="text-[#3D4E4B] dark:text-white">TOTP (Google Authenticator)</strong> dan <strong className="text-[#3D4E4B] dark:text-white">Email Verification OTP</strong>. Integrasi 2FA dirancang dengan prioritas keamanan ketat dan kontrol administrasi global.
</p> </p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-5 bg-gray-50 dark:bg-white/5 rounded-xl border border-gray-100 dark:border-white/10">
<div className="text-xs font-black text-[#D4A017] uppercase tracking-widest mb-3">📱 Google Authenticator / TOTP</div>
<ul className="space-y-2 text-xs font-medium text-gray-500 dark:text-gray-400 list-disc list-inside">
<li>Protokol standar industri RFC 6238</li>
<li>Dilengkapi 8 Recovery Codes cadangan sekali pakai</li>
<li>Dapat di-scan instan via QR Code</li>
</ul>
</div>
<div className="p-5 bg-gray-50 dark:bg-white/5 rounded-xl border border-gray-100 dark:border-white/10">
<div className="text-xs font-black text-[#D4A017] uppercase tracking-widest mb-3"> Email OTP 2FA</div>
<ul className="space-y-2 text-xs font-medium text-gray-500 dark:text-gray-400 list-disc list-inside">
<li>Pengiriman kode 6-digit dinamis langsung ke email user</li>
<li>Masa berlaku kode terbatas (TTL 10 menit)</li>
<li>Mencegah aktivasi jika SMTP Mailer belum tervalidasi</li>
</ul>
</div>
</div>
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-3">Alur Aktivasi Keamanan 2FA</h3>
<div className="space-y-3"> <div className="space-y-3">
{[ {[
{ step: '1', title: 'Buka tab Two-Factor Auth', desc: 'Masuk ke Account Settings (/settings) → tab "Two-Factor Auth"' }, { step: '1', title: 'Verifikasi SMTP / Mail Delivery', desc: 'Sebelum mengaktifkan Email 2FA, pastikan SMTP Mailer di System Settings terkonfigurasi dengan benar dan di-tes sukses handshake-nya.' },
{ step: '2', title: 'Scan QR Code', desc: 'Gunakan aplikasi authenticator (Google Authenticator / Authy) untuk scan QR' }, { step: '2', title: 'Pilih Metode di Pengaturan Pengguna', desc: 'Akses halaman Account Settings (/settings) → tab "Two-Factor Auth" untuk memilih metode yang diinginkan.' },
{ step: '3', title: 'Masukkan kode verifikasi', desc: 'Ketik 6 digit dari aplikasi untuk mengaktifkan 2FA' }, { step: '3', title: 'Konfirmasi Kode OTP / Autentikator', desc: 'Masukkan kode validasi perdana untuk memastikan setup sinkron dengan sistem sebelum metode di-lock.' },
{ step: '4', title: 'Simpan recovery codes', desc: '8 kode cadangan tersedia — simpan di tempat aman jika kehilangan akses ke authenticator' }, { step: '4', title: 'Global Admin Override (Force Disable)', desc: 'Apabila administrator menonaktifkan metode 2FA secara global di System Settings, bypass otomatis akan di-enforce di login screen.' },
{ step: '5', title: 'Login berikutnya', desc: 'Setelah diaktifkan, setiap login akan redirect ke halaman 2FA Challenge sebelum masuk dashboard' },
].map(s => ( ].map(s => (
<div key={s.step} className="flex items-start gap-4 p-4 bg-gray-50 dark:bg-white/5 rounded-xl"> <div key={s.step} className="flex items-start gap-4 p-4 bg-gray-50 dark:bg-white/5 rounded-xl">
<div className="w-7 h-7 rounded-full bg-[#3D4E4B] text-white text-xs font-black flex items-center justify-center shrink-0">{s.step}</div> <div className="w-7 h-7 rounded-full bg-[#3D4E4B] text-white text-xs font-black flex items-center justify-center shrink-0">{s.step}</div>
@@ -515,22 +548,20 @@ $this->authorize('user.delete');
</div> </div>
))} ))}
</div> </div>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700/40 rounded-xl">
<p className="text-xs font-bold text-blue-700 dark:text-blue-400">2FA bersifat opsional per user. Setelah diaktifkan, setiap login akan meminta kode 6 digit dari authenticator.</p>
</div> </div>
</div> </div>
</section> </section>
{/* ── SYSTEM SETTINGS ── */} {/* ── SYSTEM SETTINGS ── */}
<section> <section>
<SectionHeader id="settings" title="System Settings" badge="Super Admin" badgeColor="red" /> <SectionHeader id="settings" title="System Settings" badge="Admin / Super Admin" badgeColor="red" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-4"> <div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-4">
<p className="text-sm text-gray-500 dark:text-gray-400 font-medium">Hanya bisa diakses oleh pengguna dengan role <Badge color="red">super-admin</Badge>. Tersedia di <code className="text-xs bg-gray-100 dark:bg-white/10 px-2 py-0.5 rounded font-mono">/system-settings</code>.</p> <p className="text-sm text-gray-500 dark:text-gray-400 font-medium">Dapat diakses oleh pengguna dengan role <Badge color="red">super-admin</Badge> atau memiliki permission <Badge color="blue">settings.view</Badge>. Perubahan konfigurasi membutuhkan permission <Badge color="blue">settings.edit</Badge>. Tersedia di <code className="text-xs bg-gray-100 dark:bg-white/10 px-2 py-0.5 rounded font-mono">/system-settings</code>.</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[ {[
{ tab: 'General & Branding', items: ['Nama aplikasi', 'Logo upload', 'Teks logo fallback', 'Registrasi publik on/off', 'Verifikasi email on/off'] }, { tab: 'General & Branding', items: ['Nama aplikasi', 'Logo upload', 'Teks logo fallback', 'Registrasi publik on/off', 'Verifikasi email on/off', 'Global TOTP 2FA toggle (Aktif/Nonaktif)', 'Global Email 2FA toggle (Aktif/Nonaktif)'] },
{ tab: 'Security & OAuth', items: ['Password minimum panjang', 'Wajib huruf besar/kecil/angka/simbol', 'Google OAuth (Client ID & Secret)', 'GitHub OAuth (Client ID & Secret)'] }, { tab: 'Security & OAuth', items: ['Password minimum panjang', 'Wajib huruf besar/kecil/angka/simbol', 'Google OAuth (Client ID & Secret)', 'GitHub OAuth (Client ID & Secret)'] },
{ tab: 'Email / SMTP', items: ['Host & port SMTP', 'Enkripsi (TLS/SSL)', 'Username & password SMTP', 'From name & address', 'Test kirim email dari UI'] }, { tab: 'Email / SMTP', items: ['Host & port SMTP', 'Enkripsi (TLS/SSL)', 'Username & password SMTP', 'From name & address', 'Test kirim email handshake real-time (butuh settings.test-email)'] },
{ tab: 'Mobile App Control', items: ['Versi terbaru & minimum Android', 'URL Play Store', 'Mode maintenance mobile app', 'Pesan maintenance kustom'] }, { tab: 'Mobile App Control', items: ['Versi terbaru & minimum Android', 'URL Play Store', 'Mode maintenance mobile app', 'Pesan maintenance kustom'] },
].map(t => ( ].map(t => (
<div key={t.tab} className="p-5 bg-gray-50 dark:bg-white/5 rounded-xl"> <div key={t.tab} className="p-5 bg-gray-50 dark:bg-white/5 rounded-xl">
+114 -7
View File
@@ -17,10 +17,16 @@ registerPlugin(FilePondPluginImagePreview, FilePondPluginFileValidateType);
interface SettingsProps extends PageProps { interface SettingsProps extends PageProps {
mustVerifyEmail: boolean; mustVerifyEmail: boolean;
status?: string; status?: string;
twoFactorSettings: {
totp_allowed: boolean;
email_allowed: boolean;
};
twoFactor: { twoFactor: {
enabled: boolean; enabled: boolean;
qr_code: string | null; qr_code: string | null;
secret: string | null; secret: string | null;
email_enabled: boolean;
smtp_configured: boolean;
recovery_codes: string[]; recovery_codes: string[];
}; };
} }
@@ -64,15 +70,28 @@ function getSettingsTabFromHash(): SettingsTab {
return SETTINGS_TABS.includes(hash) ? hash : 'profile'; return SETTINGS_TABS.includes(hash) ? hash : 'profile';
} }
export default function SettingsIndex({ twoFactor }: SettingsProps) { export default function SettingsIndex({ twoFactor, twoFactorSettings }: SettingsProps) {
const { user } = usePage<PageProps>().props.auth; const { user } = usePage<PageProps>().props.auth;
const [activeTab, setActiveTab] = useState<SettingsTab>(getSettingsTabFromHash); const [activeTab, setActiveTab] = useState<SettingsTab>(() => {
const hash = getSettingsTabFromHash();
if (hash === '2fa' && !(twoFactorSettings.totp_allowed || twoFactorSettings.email_allowed)) {
return 'profile';
}
return hash;
});
useEffect(() => { useEffect(() => {
const onHashChange = () => setActiveTab(getSettingsTabFromHash()); const onHashChange = () => {
const hash = getSettingsTabFromHash();
if (hash === '2fa' && !(twoFactorSettings.totp_allowed || twoFactorSettings.email_allowed)) {
setActiveTab('profile');
} else {
setActiveTab(hash);
}
};
window.addEventListener('hashchange', onHashChange); window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange); return () => window.removeEventListener('hashchange', onHashChange);
}, []); }, [twoFactorSettings]);
const switchTab = (tab: SettingsTab) => { const switchTab = (tab: SettingsTab) => {
window.location.hash = tab; window.location.hash = tab;
@@ -150,8 +169,18 @@ export default function SettingsIndex({ twoFactor }: SettingsProps) {
setTimeout(() => setCopiedSecret(false), 2000); setTimeout(() => setCopiedSecret(false), 2000);
}; };
const handleEnable2FA = (e: React.SyntheticEvent) => { const handleEnable2FA = async (e: React.SyntheticEvent) => {
e.preventDefault(); e.preventDefault();
if (twoFactor.email_enabled) {
const warning = await swal.confirm(
'Deactivate Email 2FA?',
'Enabling Google Authenticator will automatically deactivate Email Two-Factor Authentication. Do you want to proceed?',
'Proceed'
);
if (!warning.isConfirmed) return;
}
twoFactorForm.post(route('two-factor.enable'), { twoFactorForm.post(route('two-factor.enable'), {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { twoFactorForm.reset(); swal.success('Enabled', '2FA is now active on your account.'); }, onSuccess: () => { twoFactorForm.reset(); swal.success('Enabled', '2FA is now active on your account.'); },
@@ -186,6 +215,44 @@ export default function SettingsIndex({ twoFactor }: SettingsProps) {
} }
}; };
const handleToggleEmail2FA = async (enable: boolean) => {
if (enable && !twoFactor.smtp_configured) {
Swal.fire({
icon: 'warning',
title: 'SMTP Mail Server Not Set Up',
text: 'Two-Factor Authentication via Email cannot be enabled because the system SMTP configurations are not set up yet. Please configure the SMTP settings under System Settings (or contact your Administrator) first.',
confirmButtonColor: '#3D4E4B',
});
return;
}
if (enable && twoFactor.enabled) {
const warning = await swal.confirm(
'Switch to Email 2FA?',
'Enabling Email 2FA will automatically deactivate your Google Authenticator setup. Do you want to proceed?',
'Proceed'
);
if (!warning.isConfirmed) return;
}
const { value: password } = await Swal.fire({
title: enable ? 'Enable Email 2FA' : 'Disable Email 2FA',
text: 'Enter your password to confirm.',
input: 'password',
inputPlaceholder: 'Your current password',
showCancelButton: true,
confirmButtonText: 'Confirm',
confirmButtonColor: '#3D4E4B',
});
if (password) {
router.post(route('two-factor.email.toggle'), { password, enabled: enable }, {
preserveScroll: true,
onSuccess: () => swal.success('Success', `Email Two-Factor Authentication has been ${enable ? 'enabled' : 'disabled'} successfully.`),
onError: (errs) => swal.error('Error', errs.password || 'Incorrect password.'),
});
}
};
return ( return (
<AuthenticatedLayout> <AuthenticatedLayout>
<Head title="Account Settings" /> <Head title="Account Settings" />
@@ -210,11 +277,13 @@ export default function SettingsIndex({ twoFactor }: SettingsProps) {
Security & Password Security & Password
{activeTab === 'security' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />} {activeTab === 'security' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
</button> </button>
{(twoFactorSettings.totp_allowed || twoFactorSettings.email_allowed) && (
<button type="button" onClick={() => switchTab('2fa')} <button type="button" onClick={() => switchTab('2fa')}
className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === '2fa' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}> className={`relative pb-3 px-1 mr-8 text-sm font-bold tracking-tight transition-colors ${activeTab === '2fa' ? 'text-[#3D4E4B]' : 'text-gray-400 hover:text-[#3D4E4B]'}`}>
Two-Factor Auth Two-Factor Auth
{activeTab === '2fa' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />} {activeTab === '2fa' && <span className="absolute bottom-0 left-0 w-full h-0.5 bg-[#D4A017] rounded-t-full" />}
</button> </button>
)}
<button type="button" onClick={() => switchTab('danger')} <button type="button" onClick={() => switchTab('danger')}
className={`relative pb-3 px-1 text-sm font-bold tracking-tight transition-colors ${activeTab === 'danger' ? 'text-red-600' : 'text-gray-400 hover:text-red-600'}`}> className={`relative pb-3 px-1 text-sm font-bold tracking-tight transition-colors ${activeTab === 'danger' ? 'text-red-600' : 'text-gray-400 hover:text-red-600'}`}>
Danger Zone Danger Zone
@@ -317,7 +386,7 @@ export default function SettingsIndex({ twoFactor }: SettingsProps) {
)} )}
{activeTab === '2fa' && ( {activeTab === '2fa' && (
<div className="max-w-3xl space-y-6 anim-fade"> <div className="max-w-4xl space-y-8 anim-fade">
<div className="p-5 bg-amber-50 border border-amber-200 rounded-2xl flex items-start gap-3"> <div className="p-5 bg-amber-50 border border-amber-200 rounded-2xl flex items-start gap-3">
<svg className="w-5 h-5 text-amber-500 mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}> <svg className="w-5 h-5 text-amber-500 mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
@@ -330,7 +399,8 @@ export default function SettingsIndex({ twoFactor }: SettingsProps) {
</div> </div>
</div> </div>
{!twoFactor.enabled ? ( {twoFactorSettings.totp_allowed && (
!twoFactor.enabled ? (
<SectionCard title="Setup Authenticator App" description="Scan QR code dengan Google Authenticator, Authy, atau TOTP app lainnya"> <SectionCard title="Setup Authenticator App" description="Scan QR code dengan Google Authenticator, Authy, atau TOTP app lainnya">
<div className="flex flex-col md:flex-row gap-10 items-center md:items-start"> <div className="flex flex-col md:flex-row gap-10 items-center md:items-start">
<div className="shrink-0"> <div className="shrink-0">
@@ -425,6 +495,43 @@ export default function SettingsIndex({ twoFactor }: SettingsProps) {
</div> </div>
</SectionCard> </SectionCard>
</div> </div>
)
)}
{/* Email 2FA Setup */}
{twoFactorSettings.email_allowed && (
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center shrink-0">
<svg className="w-6 h-6 text-[#D4A017]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div className="flex-1">
<div className="text-sm font-bold text-[#3D4E4B]">Two-Factor Authentication via Email</div>
<div className="text-xs text-gray-400 font-medium mt-0.5">
{twoFactor.email_enabled
? 'Email 2FA is active. Every login attempt will require an OTP code sent to your email.'
: 'Receive a 6-digit OTP code in your email to secure your login sessions.'}
</div>
{!twoFactor.smtp_configured && (
<div className="flex items-center gap-1.5 mt-2 text-[11px] font-bold text-amber-600 bg-amber-50 px-2.5 py-1 rounded-lg w-fit border border-amber-200">
<span className="text-sm"></span>
<span>SMTP Mail Server is not configured. Setup required.</span>
</div>
)}
</div>
{twoFactor.email_enabled ? (
<button onClick={() => handleToggleEmail2FA(false)} type="button"
className="h-9 px-5 text-xs font-bold text-red-500 border border-red-200 rounded-xl hover:bg-red-50 transition-all">
Disable Email 2FA
</button>
) : (
<button onClick={() => handleToggleEmail2FA(true)} type="button"
className="h-9 px-5 text-xs font-bold text-[#3D4E4B] border border-gray-200 rounded-xl hover:bg-gray-50 transition-all">
Enable Email 2FA
</button>
)}
</div>
)} )}
</div> </div>
)} )}
+14 -1
View File
@@ -126,6 +126,8 @@ export default function SystemSettings({ settings }: SystemSettingsProps) {
password_require_symbols: settings.password_require_symbols === '1' || settings.password_require_symbols === true, password_require_symbols: settings.password_require_symbols === '1' || settings.password_require_symbols === true,
password_require_numbers: settings.password_require_numbers === '1' || settings.password_require_numbers === true, password_require_numbers: settings.password_require_numbers === '1' || settings.password_require_numbers === true,
password_require_mixed_case: settings.password_require_mixed_case === '1' || settings.password_require_mixed_case === true, password_require_mixed_case: settings.password_require_mixed_case === '1' || settings.password_require_mixed_case === true,
two_factor_totp_enabled: settings.two_factor_totp_enabled === '1' || settings.two_factor_totp_enabled === true,
two_factor_email_enabled: settings.two_factor_email_enabled === '1' || settings.two_factor_email_enabled === true,
oauth_google_enabled: settings.oauth_google_enabled === '1' || settings.oauth_google_enabled === true, oauth_google_enabled: settings.oauth_google_enabled === '1' || settings.oauth_google_enabled === true,
oauth_google_client_id: settings.oauth_google_client_id || '', oauth_google_client_id: settings.oauth_google_client_id || '',
oauth_google_client_secret: settings.oauth_google_client_secret || '', oauth_google_client_secret: settings.oauth_google_client_secret || '',
@@ -237,7 +239,8 @@ export default function SystemSettings({ settings }: SystemSettingsProps) {
)} )}
{activeTab === 'security' && ( {activeTab === 'security' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 anim-fade"> <div className="space-y-8 anim-fade">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<SectionCard title="Password Standards" description="Complexity requirements for user access" delay="0.1s"> <SectionCard title="Password Standards" description="Complexity requirements for user access" delay="0.1s">
<div className="space-y-2"> <div className="space-y-2">
<ToggleItem label="Require Symbols" description="Include special characters (!@#$)" checked={data.settings.password_require_symbols} onChange={v => handleChange('password_require_symbols', v)} /> <ToggleItem label="Require Symbols" description="Include special characters (!@#$)" checked={data.settings.password_require_symbols} onChange={v => handleChange('password_require_symbols', v)} />
@@ -250,6 +253,15 @@ export default function SystemSettings({ settings }: SystemSettingsProps) {
</div> </div>
</SectionCard> </SectionCard>
<SectionCard title="Two-Factor Authentication (2FA)" description="Configure globally available 2FA options for users" delay="0.12s">
<div className="space-y-2">
<ToggleItem label="Google Authenticator (TOTP)" description="Allow users to use Authenticator Apps (Google, Authy, etc.)" checked={data.settings.two_factor_totp_enabled} onChange={v => handleChange('two_factor_totp_enabled', v)} />
<ToggleItem label="Email 2FA" description="Allow users to receive 6-digit OTP codes via email (requires SMTP configuration)" checked={data.settings.two_factor_email_enabled} onChange={v => handleChange('two_factor_email_enabled', v)} />
</div>
</SectionCard>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<SectionCard title="OAuth Providers" description="Third-party authentication protocols" delay="0.15s"> <SectionCard title="OAuth Providers" description="Third-party authentication protocols" delay="0.15s">
<div className="space-y-6"> <div className="space-y-6">
{['google', 'github'].map(prov => ( {['google', 'github'].map(prov => (
@@ -273,6 +285,7 @@ export default function SystemSettings({ settings }: SystemSettingsProps) {
</div> </div>
</SectionCard> </SectionCard>
</div> </div>
</div>
)} )}
{activeTab === 'email' && ( {activeTab === 'email' && (
+62 -13
View File
@@ -1,38 +1,73 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Head, useForm } from '@inertiajs/react'; import { Head, useForm, router } from '@inertiajs/react';
import { swal } from '@/lib/swal';
export default function TwoFactorChallenge() { interface Props {
type: 'email' | 'totp';
}
export default function TwoFactorChallenge({ type = 'totp' }: Props) {
const [useRecovery, setUseRecovery] = useState(false); const [useRecovery, setUseRecovery] = useState(false);
const { data, setData, post, processing, errors } = useForm({ code: '' }); const { data, setData, post, processing, errors } = useForm({ code: '' });
const [resending, setResending] = useState(false);
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
post(route('two-factor.verify'), { preserveScroll: true }); post(route('two-factor.verify'), { preserveScroll: true });
}; };
const handleResend = () => {
if (resending) return;
setResending(true);
router.post(route('two-factor.resend'), {}, {
preserveScroll: true,
onSuccess: () => {
setResending(false);
swal.success('Sent!', 'A new verification code has been sent to your email.');
},
onError: (err) => {
setResending(false);
swal.error('Error', err.code || 'Failed to resend verification code.');
}
});
};
const isEmail = type === 'email';
return ( return (
<div className="min-h-screen bg-[#E3EBE8] flex items-center justify-center p-4"> <div className="min-h-screen bg-[#E3EBE8] flex items-center justify-center p-4">
<Head title="Two-Factor Authentication" /> <Head title="Two-Factor Authentication" />
<div className="w-full max-w-sm"> <div className="w-full max-w-sm">
{/* Logo */} {/* Logo / Header */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-[#3D4E4B] mb-4"> <div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-[#3D4E4B] mb-4 shadow-lg shadow-[#3D4E4B]/10">
{isEmail && !useRecovery ? (
<svg className="w-7 h-7 text-[#D4A017]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
) : (
<svg className="w-7 h-7 text-[#D4A017]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}> <svg className="w-7 h-7 text-[#D4A017]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="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" /> <path strokeLinecap="round" strokeLinejoin="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> </svg>
)}
</div> </div>
<h1 className="text-xl font-black text-[#3D4E4B] tracking-tight">Two-Factor Authentication</h1> <h1 className="text-xl font-black text-[#3D4E4B] tracking-tight">Two-Factor Authentication</h1>
<p className="text-sm text-gray-500 font-medium mt-1"> <p className="text-sm text-gray-500 font-medium mt-2 leading-relaxed px-2">
{useRecovery ? 'Enter a recovery code to continue' : 'Enter the 6-digit code from your authenticator app'} {useRecovery
? 'Enter a recovery code to continue.'
: isEmail
? 'Please enter the 6-digit verification code sent to your registered email address.'
: 'Please enter the 6-digit authentication code from your authenticator app.'
}
</p> </p>
</div> </div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-8"> <div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-8">
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
<div> <div>
<label className="block text-xs font-bold text-gray-500 uppercase tracking-widest mb-2"> <label className="block text-xs font-bold text-gray-400 uppercase tracking-widest mb-2.5">
{useRecovery ? 'Recovery Code' : 'Authentication Code'} {useRecovery ? 'Recovery Code' : isEmail ? 'Email Verification Code' : 'Authenticator Code'}
</label> </label>
<input <input
type="text" type="text"
@@ -42,24 +77,37 @@ export default function TwoFactorChallenge() {
onChange={e => setData('code', e.target.value)} onChange={e => setData('code', e.target.value)}
autoFocus autoFocus
className={`w-full h-12 border rounded-xl px-4 text-center font-mono font-bold text-lg tracking-[0.4em] outline-none transition-all className={`w-full h-12 border rounded-xl px-4 text-center font-mono font-bold text-lg tracking-[0.4em] outline-none transition-all
${errors.code ? 'border-red-300 bg-red-50' : 'border-gray-200 focus:border-[#3D4E4B] focus:ring-2 focus:ring-[#3D4E4B]/10'}`} ${errors.code ? 'border-red-300 bg-red-50 focus:ring-red-100' : 'border-gray-200 focus:border-[#3D4E4B] focus:ring-2 focus:ring-[#3D4E4B]/10'}`}
placeholder={useRecovery ? 'xxxxxxxxxx-xxxxxxxxxx' : '000000'} placeholder={useRecovery ? 'xxxxxxxxxx-xxxxxxxxxx' : '000000'}
/> />
{errors.code && ( {errors.code && (
<p className="text-xs text-red-500 font-semibold mt-1.5">{errors.code}</p> <p className="text-xs text-red-500 font-semibold mt-1.5 text-center">{errors.code}</p>
)} )}
</div> </div>
<button <button
type="submit" type="submit"
disabled={processing || data.code.length < (useRecovery ? 5 : 6)} disabled={processing || data.code.length < (useRecovery ? 5 : 6)}
className="w-full h-11 bg-[#3D4E4B] text-white text-sm font-bold rounded-xl hover:bg-[#2D3A38] transition-all disabled:opacity-60" className="w-full h-11 bg-[#3D4E4B] text-white text-sm font-bold rounded-xl hover:bg-[#2D3A38] transition-all disabled:opacity-60 shadow-md shadow-[#3D4E4B]/15"
> >
{processing ? 'Verifying...' : 'Continue'} {processing ? 'Verifying...' : 'Continue'}
</button> </button>
</form> </form>
<div className="mt-6 text-center"> {isEmail && !useRecovery && (
<div className="mt-5 text-center">
<button
onClick={handleResend}
disabled={resending}
className="text-xs font-bold text-[#D4A017] hover:underline disabled:opacity-50"
>
{resending ? 'Resending code...' : "Didn't receive a code? Resend"}
</button>
</div>
)}
{!isEmail && (
<div className="mt-6 text-center border-t border-gray-50 pt-4">
<button <button
onClick={() => { setUseRecovery(!useRecovery); setData('code', ''); }} onClick={() => { setUseRecovery(!useRecovery); setData('code', ''); }}
className="text-xs font-bold text-[#3D4E4B] hover:underline" className="text-xs font-bold text-[#3D4E4B] hover:underline"
@@ -67,10 +115,11 @@ export default function TwoFactorChallenge() {
{useRecovery ? 'Use authenticator code instead' : 'Use a recovery code'} {useRecovery ? 'Use authenticator code instead' : 'Use a recovery code'}
</button> </button>
</div> </div>
)}
</div> </div>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<a href="/login" className="text-xs font-semibold text-gray-400 hover:text-[#3D4E4B]"> <a href="/login" className="text-xs font-semibold text-gray-400 hover:text-[#3D4E4B] transition-colors">
Back to login Back to login
</a> </a>
</div> </div>
@@ -0,0 +1,22 @@
@component('mail::message')
# Two-Factor Verification Code
Hello,
You are receiving this email because a login attempt was made on your account that requires **Two-Factor Authentication**.
Use the 6-digit verification code below to complete your login:
@component('mail::panel')
<div style="text-align: center;">
<h1 style="font-family: monospace; font-size: 32px; letter-spacing: 6px; margin: 0; color: #3D4E4B; font-weight: 800;">{{ $code }}</h1>
</div>
@endcomponent
*This code is valid for **10 minutes** from generation. If the code expires, you can request a new one.*
If you did not attempt to log in to your account, please ignore this email or update your password immediately to protect your credentials.
Thanks,<br>
{{ config('app.name') }}
@endcomponent
+8 -2
View File
@@ -21,6 +21,7 @@ Route::get('/api/search', \App\Http\Controllers\GlobalSearchController::class)
// Two-Factor Challenge (guest — user is not yet fully authenticated) // Two-Factor Challenge (guest — user is not yet fully authenticated)
Route::get('/two-factor/challenge', [\App\Http\Controllers\TwoFactorController::class, 'challenge'])->name('two-factor.challenge'); Route::get('/two-factor/challenge', [\App\Http\Controllers\TwoFactorController::class, 'challenge'])->name('two-factor.challenge');
Route::post('/two-factor/challenge', [\App\Http\Controllers\TwoFactorController::class, 'verify'])->name('two-factor.verify'); Route::post('/two-factor/challenge', [\App\Http\Controllers\TwoFactorController::class, 'verify'])->name('two-factor.verify');
Route::post('/two-factor/resend', [\App\Http\Controllers\TwoFactorController::class, 'resendCode'])->name('two-factor.resend');
// Dashboard // Dashboard
Route::get('/dashboard', [\App\Http\Controllers\DashboardController::class, 'index']) Route::get('/dashboard', [\App\Http\Controllers\DashboardController::class, 'index'])
@@ -37,10 +38,12 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Settings page // Settings page
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index'); Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');
// System Settings (Super-Admin only) // System Settings (Super-Admin / settings.view)
Route::middleware('can:settings.view')->group(function () {
Route::get('/system-settings', [\App\Http\Controllers\SystemSettingController::class, 'index'])->name('system.settings.index'); Route::get('/system-settings', [\App\Http\Controllers\SystemSettingController::class, 'index'])->name('system.settings.index');
Route::patch('/system-settings', [\App\Http\Controllers\SystemSettingController::class, 'update'])->name('system.settings.update'); Route::patch('/system-settings', [\App\Http\Controllers\SystemSettingController::class, 'update'])->name('system.settings.update');
Route::post('/system-settings/test-email', [\App\Http\Controllers\SystemSettingController::class, 'testEmail'])->name('system.settings.test-email'); Route::post('/system-settings/test-email', [\App\Http\Controllers\SystemSettingController::class, 'testEmail'])->name('system.settings.test-email');
});
// Users CRUD // Users CRUD
Route::get('/users', [UserController::class, 'index'])->name('users.index'); Route::get('/users', [UserController::class, 'index'])->name('users.index');
@@ -67,13 +70,16 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::post('/notifications', [\App\Http\Controllers\NotificationController::class, 'store'])->name('notifications.store'); Route::post('/notifications', [\App\Http\Controllers\NotificationController::class, 'store'])->name('notifications.store');
// Internal Docs // Internal Docs
Route::get('/documentation', fn() => Inertia::render('Docs/Index'))->name('docs.index'); Route::get('/documentation', fn() => Inertia::render('Docs/Index'))
->middleware('can:documentation.view')
->name('docs.index');
// Two-Factor Authentication // Two-Factor Authentication
Route::get('/two-factor', [\App\Http\Controllers\TwoFactorController::class, 'show'])->name('two-factor.show'); Route::get('/two-factor', [\App\Http\Controllers\TwoFactorController::class, 'show'])->name('two-factor.show');
Route::post('/two-factor/enable', [\App\Http\Controllers\TwoFactorController::class, 'enable'])->name('two-factor.enable'); Route::post('/two-factor/enable', [\App\Http\Controllers\TwoFactorController::class, 'enable'])->name('two-factor.enable');
Route::post('/two-factor/disable', [\App\Http\Controllers\TwoFactorController::class, 'disable'])->name('two-factor.disable'); Route::post('/two-factor/disable', [\App\Http\Controllers\TwoFactorController::class, 'disable'])->name('two-factor.disable');
Route::post('/two-factor/recovery-codes', [\App\Http\Controllers\TwoFactorController::class, 'regenerateCodes'])->name('two-factor.recovery-codes'); Route::post('/two-factor/recovery-codes', [\App\Http\Controllers\TwoFactorController::class, 'regenerateCodes'])->name('two-factor.recovery-codes');
Route::post('/two-factor/email', [\App\Http\Controllers\TwoFactorController::class, 'toggleEmail'])->name('two-factor.email.toggle');
// Roles & Permissions // Roles & Permissions
Route::get('/roles', [\App\Http\Controllers\RoleController::class, 'index'])->name('roles.index'); Route::get('/roles', [\App\Http\Controllers\RoleController::class, 'index'])->name('roles.index');
+168 -209
View File
@@ -1,288 +1,247 @@
#!/bin/bash #!/bin/bash
# --- Color Definitions for Premium Look --- # --- Color Definitions for Premium Terminal Styling ---
GREEN='\033[0;32m' GREEN='\033[0;32m'
BLUE='\033[0;34m' BLUE='\033[0;34m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
RED='\033[0;31m' RED='\033[0;31m'
CYAN='\033[0;36m' CYAN='\033[0;36m'
MAGENTA='\033[0;35m' MAGENTA='\033[0;35m'
WHITE='\033[1;37m'
BOLD='\033[1m' BOLD='\033[1m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
echo -e "${CYAN}======================================================${NC}" # Helper Functions for Clean Output
echo -e "${GREEN}${BOLD} 🐳 Biiproject Kit — Fully Containerized Setup 🐳 ${NC}" log_info() {
echo -e "${CYAN}======================================================${NC}" echo -e " ${BLUE}[INFO]${NC} $1"
}
# 1. Pre-flight Checks (Docker & Daemon status) log_success() {
echo -e "${CYAN}[1/8] Checking Docker & system environment...${NC}" echo -e " ${GREEN}[SUCCESS]${NC}$1"
if ! command -v docker &> /dev/null; then }
echo -e " ${RED}❌ Error: Docker is not installed on this system.${NC}"
echo -e " ${YELLOW}👉 Please install Docker and Docker Compose before running this script.${NC}"
exit 1
fi
if ! docker info &> /dev/null; then log_warning() {
echo -e " ${RED}❌ Error: Docker daemon is not running.${NC}" echo -e " ${YELLOW}[WARNING]${NC}$1"
echo -e " ${YELLOW}👉 Please start your Docker service/desktop and try again.${NC}" }
exit 1
fi log_error() {
echo -e " ${GREEN}✔ Docker is installed and running perfectly.${NC}" echo -e " ${RED}[ERROR]${NC}$1"
}
clear_screen() {
clear 2>/dev/null || printf "\033c"
}
clear_screen
# Premium Welcome Header
echo -e "${BLUE}======================================================================${NC}"
echo -e "${BOLD}${GREEN} 🐳 BIIPROJECT KIT V2 - CONTAINERIZED RUNNER & MONITOR 🐳 ${NC}"
echo -e "${BLUE}======================================================================${NC}"
echo -e " Skrip ini akan memvalidasi port, menyalakan container Docker,"
echo -e " dan mengaktifkan server pengembangan frontend otomatis."
echo -e "${BLUE}======================================================================${NC}"
echo "" echo ""
# 2. Prevent Port & Container conflicts # 1. VERIFY INSTALLATION STATE
echo -e "${CYAN}[2/8] Checking for port conflicts...${NC}" echo -e "${BOLD}${BLUE}[1/5] Memverifikasi Status Instalasi Aplikasi...${NC}"
# Ensure .env is present to parse ports # Check for .env file
if [ ! -f .env ] && [ -f .env.example ]; then if [ ! -f .env ]; then
cp .env.example .env log_warning "File konfigurasi '.env' tidak ditemukan."
log_info "Menjalankan skrip instalasi terlebih dahulu..."
sleep 1.5
if [ -f install.sh ]; then
chmod +x install.sh
./install.sh
exit $?
else
log_error "Skrip install.sh tidak ditemukan!"
exit 1
fi
fi fi
# Resolve ports dynamically from .env or defaults # Check for Composer dependencies
PORT_8000=$(grep "^APP_PORT=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"'\''') if [ ! -d vendor ] || [ ! -d node_modules ]; then
PORT_8000=${PORT_8000:-8000} log_warning "Dependensi aplikasi belum lengkap (vendor atau node_modules hilang)."
log_info "Mengalihkan ke skrip instalasi untuk setup awal..."
sleep 2
if [ -f install.sh ]; then
chmod +x install.sh
./install.sh
exit $?
else
log_error "Skrip install.sh tidak ditemukan!"
exit 1
fi
fi
PORT_5432=$(grep -E "^(FORWARD_DB_PORT|DB_PORT)=" .env 2>/dev/null | head -n1 | cut -d'=' -f2- | tr -d '"'\''') # Check Docker Installation
PORT_5432=${PORT_5432:-5432} if ! command -v docker &> /dev/null; then
log_error "Docker tidak ditemukan pada sistem. Harap instal Docker terlebih dahulu."
exit 1
fi
PORT_6379=$(grep -E "^(FORWARD_REDIS_PORT|REDIS_PORT)=" .env 2>/dev/null | head -n1 | cut -d'=' -f2- | tr -d '"'\''') # Check Docker Daemon
PORT_6379=${PORT_6379:-6379} if ! docker info &> /dev/null; then
log_error "Docker daemon tidak aktif. Harap jalankan layanan Docker Anda."
exit 1
fi
# Stop local processes using these ports # Resolve Docker Compose Command
if docker compose version &> /dev/null; then
DOCKER_COMPOSE_CMD="docker compose"
else
DOCKER_COMPOSE_CMD="docker-compose"
fi
log_success "Sistem siap dijalankan."
echo ""
# 2. RESOLVE PORTS & PREVENT CONFLICTS
echo -e "${BOLD}${BLUE}[2/5] Memeriksa Port Bebas & Konflik Container...${NC}"
# Parse ports from .env
APP_PORT=$(grep "^APP_PORT=" .env | cut -d'=' -f2- | tr -d '"'\''')
APP_PORT=${APP_PORT:-8000}
DB_PORT=$(grep -E "^(FORWARD_DB_PORT|DB_PORT)=" .env | head -n1 | cut -d'=' -f2- | tr -d '"'\''')
DB_PORT=${DB_PORT:-5432}
REDIS_PORT=$(grep -E "^(FORWARD_REDIS_PORT|REDIS_PORT)=" .env | head -n1 | cut -d'=' -f2- | tr -d '"'\''')
REDIS_PORT=${REDIS_PORT:-6379}
# Function to stop conflicting containers on a specific port
stop_conflict_on_port() {
local port=$1
local service_name=$2
local conflicting_container=$(docker ps --filter "publish=$port" --format "{{.Names}}" 2>/dev/null)
if [ ! -z "$conflicting_container" ]; then
# Exclude our own kit containers
if [[ "$conflicting_container" != "bii-web" && "$conflicting_container" != "bii-pgsql" && "$conflicting_container" != "bii-redis" ]]; then
log_warning "Port $port ($service_name) digunakan oleh container '$conflicting_container'."
log_info "Menghentikan container '$conflicting_container' untuk menghindari konflik..."
docker stop "$conflicting_container" &>/dev/null
sleep 1
fi
fi
}
# Stop local processes using fuser/lsof if available
if command -v lsof &>/dev/null && command -v fuser &>/dev/null; then if command -v lsof &>/dev/null && command -v fuser &>/dev/null; then
for port in "$PORT_8000" "$PORT_5432" "$PORT_6379"; do for port in "$APP_PORT" "$DB_PORT" "$REDIS_PORT"; do
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null ; then if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null ; then
echo -e "${YELLOW}⚠ Port $port is occupied by a local process. Releasing it...${NC}" log_warning "Port $port digunakan oleh proses lokal. Menghentikan proses..."
fuser -k $port/tcp 2>/dev/null fuser -k $port/tcp 2>/dev/null
sleep 1 sleep 1
fi fi
done done
fi fi
# Find the container names from our own docker-compose.yml to avoid stopping ourselves stop_conflict_on_port "$APP_PORT" "Web Aplikasi"
MY_CONTAINERS=$(grep "container_name:" docker-compose.yml 2>/dev/null | awk '{print $2}' | tr -d '"'\''') stop_conflict_on_port "$DB_PORT" "Database PostgreSQL"
stop_conflict_on_port "$REDIS_PORT" "Redis Cache"
stop_conflict_on_port() { log_success "Port $APP_PORT (Web), $DB_PORT (DB), dan $REDIS_PORT (Redis) siap digunakan."
local port=$1
local port_type=$2
local conflicting_container=$(docker ps --filter "publish=$port" --format "{{.Names}}" 2>/dev/null)
if [ ! -z "$conflicting_container" ]; then
local is_ours=false
for ours in $MY_CONTAINERS; do
if [ "$conflicting_container" = "$ours" ]; then
is_ours=true
break
fi
done
if [ "$is_ours" = "false" ]; then
echo -e "${YELLOW}⚠ Port $port ($port_type) is occupied by container '$conflicting_container'. Stopping conflict...${NC}"
docker stop "$conflicting_container" &>/dev/null
fi
fi
}
stop_conflict_on_port "$PORT_8000" "Web Server"
stop_conflict_on_port "$PORT_5432" "PostgreSQL Database"
stop_conflict_on_port "$PORT_6379" "Redis Cache"
echo -e " ${GREEN}✔ Ports $PORT_8000 (Web), $PORT_5432 (Postgres), and $PORT_6379 (Redis) are clear and ready.${NC}"
echo "" echo ""
# 3. Environment File (.env) check # 3. START DOCKER CONTAINER STACK
echo -e "${CYAN}[3/8] Verifying configuration environment (.env)...${NC}" echo -e "${BOLD}${BLUE}[3/5] Mengaktifkan Container Docker (Sail)...${NC}"
if [ ! -f .env ]; then $DOCKER_COMPOSE_CMD up -d
echo -e " ${YELLOW}⚠ .env file not found. Creating from template (.env.example)...${NC}"
cp .env.example .env
ENV_CREATED=true
else
echo -e " ${GREEN}✔ Environment configuration file (.env) is present.${NC}"
ENV_CREATED=false
fi
echo ""
# 4. Install Composer dependencies (Zero-Dependency Host)
echo -e "${CYAN}[4/8] Checking backend dependencies (Composer)...${NC}"
if [ ! -d vendor ]; then
echo -e " ${YELLOW}⚠ Vendor directory not found. Installing via lightweight PHP container...${NC}"
docker run --rm \
-u "$(id -u):$(id -g)" \
-v "$(pwd):/var/www/html" \
-w /var/www/html \
laravelsail/php83-composer:latest \
composer install --ignore-platform-reqs
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e " ${RED}❌ Error: Failed to install composer dependencies via Docker.${NC}" log_error "Gagal menyalakan Docker containers."
exit 1 exit 1
fi fi
echo -e " ${GREEN}✔ Composer dependencies successfully installed.${NC}" log_success "Docker containers berhasil dinyalakan."
else
echo -e " ${GREEN}✔ Composer dependencies are up to date.${NC}"
fi
echo "" echo ""
# 5. Start Docker Compose stack # 4. HEALTH CHECK & PERMISSIONS
echo -e "${CYAN}[5/8] Booting up Docker Compose containers...${NC}" echo -e "${BOLD}${BLUE}[4/5] Memvalidasi Kesehatan Layanan & Hak Akses...${NC}"
docker compose up -d
if [ $? -ne 0 ]; then
echo -e " ${RED}❌ Error: Failed to start Docker containers. Please check docker-compose.yml.${NC}"
exit 1
fi
echo -e " ${GREEN}✔ Docker Compose services booted successfully.${NC}"
echo ""
# Resolve DB container dynamically
DB_CONTAINER=$(docker compose ps --format "{{.Name}}" pgsql 2>/dev/null)
if [ -z "$DB_CONTAINER" ]; then
DB_CONTAINER=$(grep -A 10 -E "^ pgsql:" docker-compose.yml 2>/dev/null | grep "container_name:" | head -n1 | awk '{print $2}' | tr -d '"'\''')
fi
if [ -z "$DB_CONTAINER" ]; then
DB_CONTAINER="bii-kit-pgsql" DB_CONTAINER="bii-kit-pgsql"
fi
# Resolve Redis container dynamically
REDIS_CONTAINER=$(docker compose ps --format "{{.Name}}" redis 2>/dev/null)
if [ -z "$REDIS_CONTAINER" ]; then
REDIS_CONTAINER=$(grep -A 10 -E "^ redis:" docker-compose.yml 2>/dev/null | grep "container_name:" | head -n1 | awk '{print $2}' | tr -d '"'\''')
fi
if [ -z "$REDIS_CONTAINER" ]; then
REDIS_CONTAINER="bii-kit-redis" REDIS_CONTAINER="bii-kit-redis"
fi
# 6. Wait for Databases to be ready (Self-Healing) # Postgres Check
echo -e "${CYAN}[6/8] Waiting for services to be ready & configuring permissions...${NC}" echo -ne " ${YELLOW}Memeriksa PostgreSQL Database ($DB_CONTAINER)...${NC}"
echo -ne " ${YELLOW}Checking PostgreSQL database ($DB_CONTAINER)...${NC}"
RETRIES=0 RETRIES=0
MAX_RETRIES=40 MAX_RETRIES=30
while [ $RETRIES -lt $MAX_RETRIES ]; do while [ $RETRIES -lt $MAX_RETRIES ]; do
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$DB_CONTAINER" 2>/dev/null) STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$DB_CONTAINER" 2>/dev/null)
if [ "$STATUS" = "healthy" ]; then if [ "$STATUS" = "healthy" ]; then
echo -e "\r ${GREEN}✔ PostgreSQL is healthy and ready to accept connections! ${NC}" echo -e "\r ${GREEN}[SELESAI]${NC} ✔ PostgreSQL sehat dan siap! "
break
elif [ "$STATUS" = "unhealthy" ]; then
echo -e "\r ${RED}⚠ PostgreSQL health check reported unhealthy. Proceeding anyway...${NC}"
break break
elif [ -z "$STATUS" ]; then elif [ -z "$STATUS" ]; then
# Check if container is at least running
RUNNING=$(docker inspect --format='{{.State.Running}}' "$DB_CONTAINER" 2>/dev/null) RUNNING=$(docker inspect --format='{{.State.Running}}' "$DB_CONTAINER" 2>/dev/null)
if [ "$RUNNING" = "true" ]; then if [ "$RUNNING" = "true" ]; then
echo -e "\r ${GREEN}✔ PostgreSQL container is running (no healthcheck status). ${NC}" echo -e "\r ${GREEN}[SELESAI]${NC} ✔ PostgreSQL container aktif. "
break break
fi fi
fi fi
echo -n "." echo -n "."
sleep 1.5 sleep 1.2
RETRIES=$((RETRIES + 1)) RETRIES=$((RETRIES + 1))
done done
echo -ne " ${YELLOW}Checking Redis cache ($REDIS_CONTAINER)...${NC}" # Redis Check
echo -ne " ${YELLOW}Memeriksa Redis Cache ($REDIS_CONTAINER)...${NC}"
RETRIES=0 RETRIES=0
while [ $RETRIES -lt $MAX_RETRIES ]; do while [ $RETRIES -lt $MAX_RETRIES ]; do
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$REDIS_CONTAINER" 2>/dev/null) STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$REDIS_CONTAINER" 2>/dev/null)
if [ "$STATUS" = "healthy" ]; then if [ "$STATUS" = "healthy" ]; then
echo -e "\r ${GREEN}✔ Redis is healthy and ready to accept connections! ${NC}" echo -e "\r ${GREEN}[SELESAI]${NC} ✔ Redis sehat dan siap! "
break
elif [ "$STATUS" = "unhealthy" ]; then
echo -e "\r ${RED}⚠ Redis health check reported unhealthy. Proceeding anyway...${NC}"
break break
elif [ -z "$STATUS" ]; then elif [ -z "$STATUS" ]; then
# Check if container is at least running
RUNNING=$(docker inspect --format='{{.State.Running}}' "$REDIS_CONTAINER" 2>/dev/null) RUNNING=$(docker inspect --format='{{.State.Running}}' "$REDIS_CONTAINER" 2>/dev/null)
if [ "$RUNNING" = "true" ]; then if [ "$RUNNING" = "true" ]; then
echo -e "\r ${GREEN}✔ Redis container is running (no healthcheck status). ${NC}" echo -e "\r ${GREEN}[SELESAI]${NC} ✔ Redis container aktif. "
break break
fi fi
fi fi
echo -n "." echo -n "."
sleep 1.5 sleep 1.2
RETRIES=$((RETRIES + 1)) RETRIES=$((RETRIES + 1))
done done
# Extra sleep to ensure Docker internal DNS has fully propagated the container names # Secure storage and cache permissions inside container
sleep 2 $DOCKER_COMPOSE_CMD exec -u root laravel.test chown -R sail:sail /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null
$DOCKER_COMPOSE_CMD exec -u root laravel.test chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null
# Correct any permission issues on build and storage folders before continuing log_success "Hak akses file storage & cache berhasil diselaraskan."
echo -ne " ${YELLOW}Securing workspace file permissions...${NC}"
docker compose exec -u root laravel.test chown -R sail:sail /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null
docker compose exec -u root laravel.test chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null
if [ -d public/build ]; then
docker compose exec -u root laravel.test chown -R sail:sail /var/www/html/public/build 2>/dev/null
docker compose exec -u root laravel.test chmod -R 775 /var/www/html/public/build 2>/dev/null
fi
echo -e "\r ${GREEN}✔ Workspace file permissions secured. ${NC}"
echo "" echo ""
# 7. Securing Application & Database Initialization # 5. START VITE FRONTEND LIVE SERVER
echo -e "${CYAN}[7/8] Securely initializing application database & keys...${NC}" echo -e "${BOLD}${BLUE}[5/5] Memulai Server Frontend (Vite Live-Reload)...${NC}"
# Check for empty key # Start npm dev in background inside container
APP_KEY_VAL=$(grep "^APP_KEY=" .env | cut -d'=' -f2- | tr -d '"'\''') $DOCKER_COMPOSE_CMD exec -d -u sail laravel.test npm run dev
if [ -z "$APP_KEY_VAL" ] || [ "$APP_KEY_VAL" = "base64:" ] || [ "$APP_KEY_VAL" = "SomeRandomString" ]; then log_success "Server pengembangan Vite aktif di latar belakang."
echo -e " ${YELLOW}⚙ Generating unique application encryption key...${NC}"
docker compose exec -u sail laravel.test php artisan key:generate
fi
# Check if tables exist to decide fresh migrate & seed or regular migrate
TABLE_EXISTS=$(docker compose exec -u sail laravel.test php artisan tinker --execute="echo Schema::hasTable('users') ? 'yes' : 'no';" 2>/dev/null | tr -d '\r\n')
if [ "$TABLE_EXISTS" != "yes" ]; then
echo -e " ${YELLOW}⚙ Database is uninitialized. Running migrations & seeding default accounts...${NC}"
docker compose exec -u sail laravel.test php artisan migrate:fresh --seed
else
USER_COUNT=$(docker compose exec -u sail laravel.test php artisan tinker --execute="echo App\Models\User::count();" 2>/dev/null | tr -d '\r\n')
if [[ -z "$USER_COUNT" || "$USER_COUNT" == "0" ]]; then
echo -e " ${YELLOW}⚙ Database has tables but no records. Seeding default accounts...${NC}"
docker compose exec -u sail laravel.test php artisan db:seed
else
echo -e " ${GREEN}✔ Database already initialized with $USER_COUNT users. Running pending migrations...${NC}"
docker compose exec -u sail laravel.test php artisan migrate
fi
fi
echo "" echo ""
# 8. Frontend Assets (NPM dependencies and dev server) sleep 1
echo -e "${CYAN}[8/8] Preparing frontend assets & live-reload server...${NC}" clear_screen
if [ ! -d node_modules ]; then
echo -e " ${YELLOW}⚙ Node modules not found. Installing NPM packages inside container...${NC}"
docker compose exec -u sail laravel.test npm install
else
echo -e " ${GREEN}✔ Node dependencies are already present.${NC}"
fi
# Ensure storage symlink exists # Premium Dashboard Information Board
docker compose exec -u sail laravel.test php artisan storage:link &>/dev/null echo -e "${GREEN}======================================================================${NC}"
echo -e "${BOLD}${GREEN} 🚀 BIIPROJECT KIT V2 BERHASIL DIJALANKAN! 🚀 ${NC}"
# Clean up permissions again in case npm install created any root-owned folders echo -e "${GREEN}======================================================================${NC}"
docker compose exec -u root laravel.test chown -R sail:sail /var/www/html/node_modules /var/www/html/package-lock.json 2>/dev/null echo -e " Aplikasi Anda berjalan lancar di dalam Docker container."
echo -e ""
# Build production assets first to ensure page loads immediately echo -e " 👉 ${BOLD}Web Application:${NC} ${CYAN}http://localhost:${APP_PORT}${NC}"
echo -e " ${YELLOW}⚙ Compiling frontend production assets (Vite)...${NC}" echo -e " 👉 ${BOLD}Monitoring Panel:${NC} ${CYAN}http://localhost:${APP_PORT}/system-monitoring${NC}"
docker compose exec -u sail laravel.test npm run build echo -e " 👉 ${BOLD}Dokumentasi API:${NC} ${CYAN}http://localhost:${APP_PORT}/documentation${NC}"
echo -e " 👉 ${BOLD}Database Port:${NC} ${YELLOW}${DB_PORT}${NC} (PostgreSQL)"
# Start Vite live-reload server in background echo -e " 👉 ${BOLD}Redis Port:${NC} ${YELLOW}${REDIS_PORT}${NC} (Redis)"
echo -e " ${GREEN}✔ Production assets compiled. Launching Vite live-reload server in background...${NC}" echo -e ""
docker compose exec -d -u sail laravel.test npm run dev echo -e " ========================================================"
echo -e " ${BOLD}PERINTAH BERMANFAAT LAINNYA:${NC}"
echo -e " ========================================================"
echo -e " Untuk melihat logs realtime dari container:"
echo -e " ${CYAN}docker compose logs -f${NC}"
echo -e ""
echo -e " Untuk masuk ke terminal container aplikasi:"
echo -e " ${CYAN}./sail shell${NC}"
echo -e ""
echo -e " Untuk menghentikan dan menonaktifkan container:"
echo -e " ${CYAN}docker compose down${NC}"
echo -e "${GREEN}======================================================================${NC}"
echo "" echo ""
# Retrieve ports for display
PORT_8000=$(grep "^APP_PORT=" .env | cut -d'=' -f2- | tr -d '"'\''')
PORT_8000=${PORT_8000:-8000}
echo -e "${CYAN}=========================================================${NC}"
echo -e " 🎉 ${GREEN}${BOLD}Biiproject Kit is running fully in Docker!${NC} 🎉"
echo -e "${CYAN}=========================================================${NC}"
echo -e " 🚀 ${WHITE}${BOLD}Web Application:${NC} ${GREEN}http://localhost:${PORT_8000}${NC}"
echo -e " 📋 ${WHITE}${BOLD}Activity Telemetry:${NC} ${GREEN}http://localhost:${PORT_8000}/activity-logs${NC}"
echo -e " 📖 ${WHITE}${BOLD}Documentation Hub:${NC} ${GREEN}http://localhost:${PORT_8000}/documentation${NC}"
echo -e " 🐘 ${WHITE}${BOLD}Database Port (Postgres):${NC} ${YELLOW}5432${NC}"
echo -e " 🍒 ${WHITE}${BOLD}Redis Port (Cache):${NC} ${YELLOW}6379${NC}"
echo -e "${CYAN}=========================================================${NC}"
echo -e " ${YELLOW}${BOLD}Useful Commands:${NC}"
echo -e " 👉 View live logs: ${CYAN}docker compose logs -f${NC}"
echo -e " 👉 Stop the stack: ${CYAN}docker compose down${NC}"
echo -e " 👉 Restart containers: ${CYAN}./run.sh${NC}"
echo -e "${CYAN}=========================================================${NC}"
+91
View File
@@ -0,0 +1,91 @@
<?php
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use App\Mail\Send2FACode;
test('users with email 2fa enabled are redirected to challenge page', function () {
Mail::fake();
$user = User::factory()->create([
'email_2fa_enabled' => true,
]);
$response = $this->post('/login', [
'email' => $user->email,
'password' => 'password',
]);
$this->assertGuest();
$response->assertRedirect(route('two-factor.challenge'));
$user->refresh();
expect($user->email_2fa_code)->not->toBeNull();
expect($user->email_2fa_expires_at)->not->toBeNull();
Mail::assertSent(Send2FACode::class, function ($mail) use ($user) {
return $mail->hasTo($user->email) && $mail->code === $user->email_2fa_code;
});
$this->assertEquals($user->id, session('two_factor_user_id'));
$this->assertEquals('email', session('two_factor_type'));
});
test('users can verify their email 2fa code successfully', function () {
$user = User::factory()->create([
'email_2fa_enabled' => true,
'email_2fa_code' => '123456',
'email_2fa_expires_at' => now()->addMinutes(10),
]);
session(['two_factor_user_id' => $user->id, 'two_factor_type' => 'email']);
$response = $this->post('/two-factor/challenge', [
'code' => '123456',
]);
$this->assertAuthenticatedAs($user);
$response->assertRedirect(route('dashboard'));
$user->refresh();
expect($user->email_2fa_code)->toBeNull();
expect($user->email_2fa_expires_at)->toBeNull();
});
test('users cannot verify incorrect email 2fa code', function () {
$user = User::factory()->create([
'email_2fa_enabled' => true,
'email_2fa_code' => '123456',
'email_2fa_expires_at' => now()->addMinutes(10),
]);
session(['two_factor_user_id' => $user->id, 'two_factor_type' => 'email']);
$response = $this->post('/two-factor/challenge', [
'code' => '654321',
]);
$this->assertGuest();
$response->assertSessionHasErrors(['code']);
});
test('users can resend email 2fa code', function () {
Mail::fake();
$user = User::factory()->create([
'email_2fa_enabled' => true,
'email_2fa_code' => '123456',
'email_2fa_expires_at' => now()->addMinutes(10),
]);
session(['two_factor_user_id' => $user->id, 'two_factor_type' => 'email']);
$response = $this->post('/two-factor/resend');
$response->assertSessionHasNoErrors();
$user->refresh();
expect($user->email_2fa_code)->not->toEqual('123456');
Mail::assertSent(Send2FACode::class);
});